diff --git a/src/adafruit_circuitplayground/constants.py b/src/adafruit_circuitplayground/constants.py index 98924bfcc..505fd5d57 100644 --- a/src/adafruit_circuitplayground/constants.py +++ b/src/adafruit_circuitplayground/constants.py @@ -27,12 +27,21 @@ VALID_PIXEL_ASSIGN_ERROR = "The pixel color value should be a tuple with three values between 0 and 255 or a hexadecimal color between 0x000000 and 0xFFFFFF." +TELEMETRY_EVENT_NAMES = { + "TAPPED": "API.TAPPED", + "PLAY_FILE": "API.PLAY.FILE", + "PLAY_TONE": "API.PLAY.TONE", + "START_TONE": "API.START.TONE", + "STOP_TONE": "API.STOP.TONE", + "DETECT_TAPS": "API.DETECT.TAPS", + "ADJUST_THRESHOLD": "API.ADJUST.THRESHOLD", + "RED_LED": "API.RED.LED", + "PIXELS": "API.PIXELS", +} ERROR_SENDING_EVENT = "Error trying to send event to the process : " TIME_DELAY = 0.03 -DEFAULT_PORT = "5577" - EVENTS_BUTTON_PRESS = ["button_a", "button_b", "switch"] EVENTS_SENSOR_CHANGED = ["temperature", "light", "motion_x", "motion_y", "motion_z"] diff --git a/src/adafruit_circuitplayground/debugger_communication_client.py b/src/adafruit_circuitplayground/debugger_communication_client.py deleted file mode 100644 index 1a28ff445..000000000 --- a/src/adafruit_circuitplayground/debugger_communication_client.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -import json -import socketio -import copy -from . import express -from . import constants as CONSTANTS -from common import utils - - -previous_state = {} - -# similar to utils.send_to_simulator, but for debugging -# (needs handle to device-specific debugger) -def debug_send_to_simulator(state): - global previous_state - - if state != previous_state: - previous_state = copy.deepcopy(state) - - updated_state = utils.update_state_with_device_name(state, CONSTANTS.CPX) - message = utils.create_message(updated_state) - - update_state(json.dumps(message)) - - -# Create Socket Client -sio = socketio.Client(reconnection_attempts=2) - -# TODO: Get port from process_user_code.py via childprocess communication - - -# Initialize connection -def init_connection(port=CONSTANTS.DEFAULT_PORT): - sio.connect("http://localhost:{}".format(port)) - - -# Transfer the user's inputs to the API -def __update_api_state(data, expected_events): - try: - event_state = json.loads(data) - for event in expected_events: - express.cpx._Express__state[event] = event_state.get( - event, express.cpx._Express__state[event] - ) - except Exception as e: - print(CONSTANTS.ERROR_SENDING_EVENT, e, file=sys.stderr, flush=True) - - -# Method : Update State -def update_state(state): - sio.emit("updateState", state) - - -## Events Handlers ## - - -# Event : Button pressed (A, B, A+B, Switch) -@sio.on("button_press") -def button_press(data): - __update_api_state(data, CONSTANTS.EVENTS_BUTTON_PRESS) - - -# Event : Sensor changed (Temperature, light, Motion) -@sio.on("sensor_changed") -def sensor_changed(data): - __update_api_state(data, CONSTANTS.EVENTS_SENSOR_CHANGED) diff --git a/src/adafruit_circuitplayground/express.py b/src/adafruit_circuitplayground/express.py index 5eb9a3d7e..884f4a723 100644 --- a/src/adafruit_circuitplayground/express.py +++ b/src/adafruit_circuitplayground/express.py @@ -12,7 +12,8 @@ from .pixel import Pixel from . import constants as CONSTANTS from collections import namedtuple -from . import debugger_communication_client +from applicationinsights import TelemetryClient +import common Acceleration = namedtuple("acceleration", ["x", "y", "z"]) @@ -115,7 +116,9 @@ def light(self): def __show(self): if self.__debug_mode: - debugger_communication_client.debug_send_to_simulator(self.__state) + common.debugger_communication_client.debug_send_to_simulator( + self.__state, CONSTANTS.CPX + ) else: utils.send_to_simulator(self.__state, CONSTANTS.CPX) diff --git a/src/adafruit_circuitplayground/pixel.py b/src/adafruit_circuitplayground/pixel.py index 410f2861d..18e0028fc 100644 --- a/src/adafruit_circuitplayground/pixel.py +++ b/src/adafruit_circuitplayground/pixel.py @@ -3,12 +3,12 @@ import json import sys +import common from common import utils from common.telemetry import telemetry_py from common.telemetry_events import TelemetryEvent from . import constants as CONSTANTS -from . import debugger_communication_client class Pixel: @@ -22,9 +22,11 @@ def show(self): # Send the state to the extension so that React re-renders the Webview # or send the state to the debugger (within this library) if self.__debug_mode: - debugger_communication_client.debug_send_to_simulator(self.__state) + common.debugger_communication_client.debug_send_to_simulator( + self.__state, CONSTANTS.CPX + ) else: - utils.send_to_simulator(self.__state, CONSTANTS.CPX) + common.utils.send_to_simulator(self.__state, CONSTANTS.CPX) def __show_if_auto_write(self): if self.auto_write: diff --git a/src/adafruit_circuitplayground/test/test_debugger_communication_client.py b/src/adafruit_circuitplayground/test/test_debugger_communication_client.py deleted file mode 100644 index b25e27eb8..000000000 --- a/src/adafruit_circuitplayground/test/test_debugger_communication_client.py +++ /dev/null @@ -1,63 +0,0 @@ -import pytest -import json # Remove -from unittest import mock -import socketio - -from .. import express -from .. import debugger_communication_client - - -class TestDebuggerCommunicationClient(object): - @mock.patch("socketio.Client.connect") - def test_init_connection(self, mock_connect): - mock_connect.return_value = None - debugger_communication_client.init_connection() - mock_connect.assert_called_once() - - def test_init_connection1(self): - socketio.Client.connect = mock.Mock() - socketio.Client.connect.return_value = None - debugger_communication_client.init_connection() - socketio.Client.connect.assert_called_once() - - def test_update_state(self): - socketio.Client.emit = mock.Mock() - socketio.Client.emit.return_value = None - debugger_communication_client.update_state({}) - socketio.Client.emit.assert_called_once() - - @mock.patch.dict( - express.cpx._Express__state, - {"button_a": False, "button_b": False, "switch": True}, - clear=True, - ) - def test_button_press(self): - data = {"button_a": True, "button_b": True, "switch": True} - serialized_data = json.dumps(data) - debugger_communication_client.button_press(serialized_data) - assert data == express.cpx._Express__state - - @mock.patch.dict( - express.cpx._Express__state, - {"temperature": 0, "light": 0, "motion_x": 0, "motion_y": 0, "motion_z": 0}, - clear=True, - ) - def test_sensor_changed(self): - data = { - "temperature": 1, - "light": 2, - "motion_x": 3, - "motion_y": 4, - "motion_z": 5, - } - serialized_data = json.dumps(data) - debugger_communication_client.sensor_changed(serialized_data) - assert data == express.cpx._Express__state - - @mock.patch("builtins.print") - @mock.patch.dict(express.cpx._Express__state, {}, clear=True) - def test_update_api_state_fail(self, mocked_print): - data = [] - debugger_communication_client.sensor_changed(data) - # Exception is caught and a print is stated to stderr - mocked_print.assert_called_once() diff --git a/src/common/constants.py b/src/common/constants.py index 97ea32386..73984933a 100644 --- a/src/common/constants.py +++ b/src/common/constants.py @@ -1,3 +1,10 @@ MAC_OS = "darwin" +ERROR_SENDING_EVENT = "Error trying to send event to the process : " + +ACTIVE_DEVICE_FIELD = "active_device" +STATE_FIELD = "state" + +CONNECTION_ATTEMPTS = 10 TIME_DELAY = 0.03 +DEFAULT_PORT = "5577" diff --git a/src/common/debugger_communication_client.py b/src/common/debugger_communication_client.py new file mode 100644 index 000000000..4760020d0 --- /dev/null +++ b/src/common/debugger_communication_client.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +import json +import socketio +import copy + +from . import constants as CONSTANTS +from . import utils +import threading + + +from adafruit_circuitplayground.express import cpx +from adafruit_circuitplayground.constants import CPX + +from microbit.__model.microbit_model import __mb as mb +from microbit.__model.constants import MICROBIT + + +device_dict = {CPX: cpx, MICROBIT: mb} +processing_state_event = threading.Event() +previous_state = {} + +# similar to utils.send_to_simulator, but for debugging +# (needs handle to device-specific debugger) +def debug_send_to_simulator(state, active_device): + global previous_state + if state != previous_state: + previous_state = copy.deepcopy(state) + + updated_state = utils.update_state_with_device_name(state, active_device) + message = utils.create_message(updated_state) + + update_state(json.dumps(message)) + + +# Create Socket Client +sio = socketio.Client(reconnection_attempts=CONSTANTS.CONNECTION_ATTEMPTS) + +# TODO: Get port from process_user_code.py via childprocess communication + + +# Initialize connection +def init_connection(port=CONSTANTS.DEFAULT_PORT): + sio.connect("http://localhost:{}".format(port)) + + +# Transfer the user's inputs to the API +def __update_api_state(data): + try: + event_state = json.loads(data) + active_device_string = event_state.get(CONSTANTS.ACTIVE_DEVICE_FIELD) + + if active_device_string is not None: + active_device = device_dict.get(active_device_string) + if active_device is not None: + active_device.update_state(event_state.get(CONSTANTS.STATE_FIELD)) + + except Exception as e: + print(CONSTANTS.ERROR_SENDING_EVENT, e, file=sys.stderr, flush=True) + + +# Method : Update State +def update_state(state): + processing_state_event.clear() + sio.emit("updateState", state) + processing_state_event.wait() + + +# Event : Button pressed (A, B, A+B, Switch) +# or Sensor changed (Temperature, light, Motion) +@sio.on("input_changed") +def input_changed(data): + sio.emit("receivedState", data) + __update_api_state(data) + + +@sio.on("received_state") +def received_state(data): + processing_state_event.set() diff --git a/src/common/test/test_debugger_communication_client.py b/src/common/test/test_debugger_communication_client.py new file mode 100644 index 000000000..0706ad149 --- /dev/null +++ b/src/common/test/test_debugger_communication_client.py @@ -0,0 +1,178 @@ +import pytest +import json # Remove +from unittest import mock +import socketio +import threading + +from adafruit_circuitplayground import express +from common import debugger_communication_client +from common import constants as CONSTANTS +from adafruit_circuitplayground.constants import CPX + + +class TestDebuggerCommunicationClient(object): + @mock.patch("socketio.Client.connect") + def test_init_connection(self, mock_connect): + mock_connect.return_value = None + debugger_communication_client.init_connection() + mock_connect.assert_called_once() + + def test_init_connection1(self): + socketio.Client.connect = mock.Mock() + socketio.Client.connect.return_value = None + debugger_communication_client.init_connection() + socketio.Client.connect.assert_called_once() + + def test_update_state(self): + threading.Event.clear = mock.Mock() + threading.Event.wait = mock.Mock() + socketio.Client.emit = mock.Mock() + socketio.Client.emit.return_value = None + debugger_communication_client.update_state( + {CONSTANTS.ACTIVE_DEVICE_FIELD: CPX, CONSTANTS.STATE_FIELD: {}} + ) + socketio.Client.emit.assert_called_once() + + @mock.patch.dict( + express.cpx._Express__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, + }, + clear=True, + ) + def test_button_press(self): + data = { + CONSTANTS.ACTIVE_DEVICE_FIELD: CPX, + CONSTANTS.STATE_FIELD: {"button_a": True, "button_b": True, "switch": True}, + } + expected_data = { + "brightness": 1.0, + "button_a": True, + "button_b": True, + "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": True, + "temperature": 0, + "light": 0, + "motion_x": 0, + "motion_y": 0, + "motion_z": 0, + "touch": [False] * 7, + "shake": False, + } + serialized_data = json.dumps(data) + debugger_communication_client.input_changed(serialized_data) + assert expected_data == express.cpx._Express__state + + @mock.patch.dict( + express.cpx._Express__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, + }, + clear=True, + ) + def test_input_changed(self): + data = { + CONSTANTS.ACTIVE_DEVICE_FIELD: CPX, + CONSTANTS.STATE_FIELD: { + "temperature": 1, + "light": 2, + "motion_x": 3, + "motion_y": 4, + "motion_z": 5, + }, + } + expected_data = { + "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": 1, + "light": 2, + "motion_x": 3, + "motion_y": 4, + "motion_z": 5, + "touch": [False] * 7, + "shake": False, + } + serialized_data = json.dumps(data) + debugger_communication_client.input_changed(serialized_data) + assert expected_data == express.cpx._Express__state + + @mock.patch("builtins.print") + @mock.patch.dict(express.cpx._Express__state, {}, clear=True) + def test_update_api_state_fail(self, mocked_print): + data = [] + debugger_communication_client.input_changed(data) + # Exception is caught and a print is stated to stderr + mocked_print.assert_called_once() diff --git a/src/debug_user_code.py b/src/debug_user_code.py index 8d5b57873..2ea654ac0 100644 --- a/src/debug_user_code.py +++ b/src/debug_user_code.py @@ -20,7 +20,8 @@ # This import must happen after the sys.path is modified from adafruit_circuitplayground.express import cpx -from adafruit_circuitplayground import debugger_communication_client +from microbit.__model.microbit_model import __mb as mb +from common import debugger_communication_client ## Execute User Code ## @@ -45,6 +46,7 @@ cpx._Express__abs_path_to_code_file = abs_path_to_code_file cpx._Express__debug_mode = True cpx.pixels._Pixel__set_debug_mode(True) +mb._MicrobitModel__set_debug_mode(True) # Execute the user's code file with open(abs_path_to_code_file) as user_code_file: diff --git a/src/debuggerCommunicationServer.ts b/src/debuggerCommunicationServer.ts index 76eb02f42..1d73b8c29 100644 --- a/src/debuggerCommunicationServer.ts +++ b/src/debuggerCommunicationServer.ts @@ -6,15 +6,32 @@ import * as socketio from "socket.io"; import { WebviewPanel } from "vscode"; import { SERVER_INFO } from "./constants"; +const DEBUGGER_MESSAGES = { + EMITTER: { + INPUT_CHANGED: "input_changed", + RECEIVED_STATE: "received_state", + DISCONNECT: "frontend_disconnected", + }, + LISTENER: { + UPDATE_STATE: "updateState", + RECEIVED_STATE: "receivedState", + DISCONNECT: "disconnect", + }, +}; + export class DebuggerCommunicationServer { private port: number; private serverHttp: http.Server; private serverIo: socketio.Server; private simulatorWebview: WebviewPanel | undefined; + private currentActiveDevice; + private isPendingResponse = false; + private pendingCallbacks: Array = []; constructor( webviewPanel: WebviewPanel | undefined, - port = SERVER_INFO.DEFAULT_SERVER_PORT + port = SERVER_INFO.DEFAULT_SERVER_PORT, + currentActiveDevice: string ) { this.port = port; this.serverHttp = new http.Server(); @@ -24,6 +41,8 @@ export class DebuggerCommunicationServer { this.simulatorWebview = webviewPanel; this.initEventsHandlers(); console.info(`Server running on port ${this.port}`); + + this.currentActiveDevice = currentActiveDevice; } public closeConnection(): void { @@ -35,17 +54,22 @@ export class DebuggerCommunicationServer { public setWebview(webviewPanel: WebviewPanel | undefined) { this.simulatorWebview = webviewPanel; } - - // Emit Buttons Inputs Events - public emitButtonPress(newState: string): void { - console.log(`Emit Button Press: ${newState} \n`); - this.serverIo.emit("button_press", newState); - } - - // Emit Sensors Inputs Events - public emitSensorChanged(newState: string): void { - console.log(`Emit Sensor Changed: ${newState} \n`); - this.serverIo.emit("sensor_changed", newState); + // Events are pushed when the previous processed event is over + public emitInputChanged(newState: string): void { + if (this.isPendingResponse) { + this.pendingCallbacks.push(() => { + this.serverIo.emit( + DEBUGGER_MESSAGES.EMITTER.INPUT_CHANGED, + newState + ); + }); + } else { + this.serverIo.emit( + DEBUGGER_MESSAGES.EMITTER.INPUT_CHANGED, + newState + ); + this.isPendingResponse = true; + } } private initHttpServer(): void { @@ -57,14 +81,25 @@ export class DebuggerCommunicationServer { private initEventsHandlers(): void { this.serverIo.on("connection", (socket: any) => { - console.log("Connection received"); - - socket.on("updateState", (data: any) => { + socket.on(DEBUGGER_MESSAGES.LISTENER.UPDATE_STATE, (data: any) => { this.handleState(data); + this.serverIo.emit( + DEBUGGER_MESSAGES.EMITTER.RECEIVED_STATE, + {} + ); + }); + socket.on(DEBUGGER_MESSAGES.LISTENER.RECEIVED_STATE, () => { + if (this.pendingCallbacks.length > 0) { + const currentCall = this.pendingCallbacks.shift(); + currentCall(); + this.isPendingResponse = true; + } else { + this.isPendingResponse = false; + } }); - socket.on("disconnect", () => { - console.log("Socket disconnected"); + socket.on(DEBUGGER_MESSAGES.LISTENER.DISCONNECT, () => { + this.serverIo.emit(DEBUGGER_MESSAGES.EMITTER.DISCONNECT, {}); if (this.simulatorWebview) { this.simulatorWebview.webview.postMessage({ command: "reset-state", @@ -81,6 +116,7 @@ export class DebuggerCommunicationServer { console.log(`State recieved: ${messageToWebview.data}`); if (this.simulatorWebview) { this.simulatorWebview.webview.postMessage({ + active_device: this.currentActiveDevice, command: "set-state", state: JSON.parse(messageToWebview.data), }); diff --git a/src/extension.ts b/src/extension.ts index 21c9b31d5..0f221bad2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -178,7 +178,7 @@ export async function activate(context: vscode.ExtensionContext) { inDebugMode && debuggerCommunicationHandler ) { - debuggerCommunicationHandler.emitButtonPress( + debuggerCommunicationHandler.emitInputChanged( messageJson ); } else if (childProcess) { @@ -225,7 +225,7 @@ export async function activate(context: vscode.ExtensionContext) { inDebugMode && debuggerCommunicationHandler ) { - debuggerCommunicationHandler.emitSensorChanged( + debuggerCommunicationHandler.emitInputChanged( messageJson ); } else if (childProcess) { @@ -931,7 +931,8 @@ export async function activate(context: vscode.ExtensionContext) { debuggerCommunicationHandler = new DebuggerCommunicationServer( currentPanel, - utils.getServerPortConfig() + utils.getServerPortConfig(), + currentActiveDevice ); handleDebuggerTelemetry(); @@ -1305,6 +1306,7 @@ function getWebviewContent(context: vscode.ExtensionContext) { + ${loadScript(context, "out/vendor.js")} ${loadScript(context, "out/simulator.js")} diff --git a/src/microbit/__model/display.py b/src/microbit/__model/display.py index 791e1ea8b..eee651064 100644 --- a/src/microbit/__model/display.py +++ b/src/microbit/__model/display.py @@ -1,6 +1,7 @@ import copy import time import threading +import common from common import utils from common.telemetry import telemetry_py @@ -20,6 +21,7 @@ def __init__(self): self.__current_pid = None self.__lock = threading.Lock() + self.__debug_mode = False def scroll(self, value, delay=150, wait=True, loop=False, monospace=False): """ @@ -349,7 +351,13 @@ def __create_scroll_image(images): def __update_client(self): sendable_json = {"leds": self.__get_array()} - utils.send_to_simulator(sendable_json, CONSTANTS.MICROBIT) + + if self.__debug_mode: + common.debugger_communication_client.debug_send_to_simulator( + sendable_json, CONSTANTS.MICROBIT + ) + else: + common.utils.send_to_simulator(sendable_json, CONSTANTS.MICROBIT) def __update_light_level(self, new_light_level): if new_light_level is not None: diff --git a/src/microbit/__model/microbit_model.py b/src/microbit/__model/microbit_model.py index cf00452be..e1bbb8971 100644 --- a/src/microbit/__model/microbit_model.py +++ b/src/microbit/__model/microbit_model.py @@ -71,5 +71,8 @@ def __update_temp(self, new_state): if new_temp != previous_temp: self._MicrobitModel__set_temperature(new_temp) + def __set_debug_mode(self, mode): + self.display._Display__debug_mode = mode + __mb = MicrobitModel()