diff --git a/src/base_circuitpython/base_cp_constants.py b/src/base_circuitpython/base_cp_constants.py index 3736fa333..bd93e5576 100644 --- a/src/base_circuitpython/base_cp_constants.py +++ b/src/base_circuitpython/base_cp_constants.py @@ -26,6 +26,9 @@ class CLUE_STATE: GYRO_X = "gyro_x" GYRO_Y = "gyro_y" GYRO_Z = "gyro_z" + # LEDs + RED_LED = "red_led" + WHITE_LEDS = "white_leds" CPX = "CPX" diff --git a/src/clue/adafruit_clue.py b/src/clue/adafruit_clue.py index 8a6d6fcae..a44794c98 100644 --- a/src/clue/adafruit_clue.py +++ b/src/clue/adafruit_clue.py @@ -56,6 +56,11 @@ https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel """ +from common.telemetry_events import TelemetryEvent +from common.telemetry import telemetry_py +from common import utils +from base_circuitpython import base_cp_constants as CONSTANTS +import neopixel import time import array import math @@ -67,11 +72,6 @@ abs_path = pathlib.Path(__file__).parent.absolute() sys.path.insert(0, os.path.join(abs_path)) -import neopixel -from base_circuitpython import base_cp_constants as CONSTANTS -from common import utils -from common.telemetry import telemetry_py -from common.telemetry_events import TelemetryEvent # REVISED VERSION OF THE ADAFRUIT CLUE LIBRARY FOR DSX @@ -227,6 +227,10 @@ def __init__(self): self.__state[CONSTANTS.CLUE_STATE.GYRO_X] = 0 self.__state[CONSTANTS.CLUE_STATE.GYRO_Y] = 0 self.__state[CONSTANTS.CLUE_STATE.GYRO_Z] = 0 + # LEDs + self.__state[CONSTANTS.CLUE_STATE.RED_LED] = False + self.__state[CONSTANTS.CLUE_STATE.WHITE_LEDS] = False + self.button_mapping = { CONSTANTS.CLUE_STATE.BUTTON_A: "A", CONSTANTS.CLUE_STATE.BUTTON_B: "B", @@ -502,7 +506,7 @@ def touch_0(self): @property def touch_1(self): """Not Implemented! - + Detect touch on capacitive touch pad 1. .. image :: ../docs/_static/pad_1.jpg :alt: Pad 1 @@ -520,7 +524,7 @@ def touch_1(self): @property def touch_2(self): """Not Implemented! - + Detect touch on capacitive touch pad 2. .. image :: ../docs/_static/pad_2.jpg :alt: Pad 2 @@ -537,9 +541,7 @@ def touch_2(self): @property def white_leds(self): - """Not Implemented! - - The red led next to the USB plug labeled LED. + """The red led next to the USB plug labeled LED. .. image :: ../docs/_static/white_leds.jpg :alt: White LEDs This example turns on the white LEDs. @@ -549,19 +551,16 @@ def white_leds(self): clue.white_leds = True """ telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_WHITE_LEDS) - utils.print_for_unimplemented_functions(Clue.white_leds.__name__) + return self.__state[CONSTANTS.CLUE_STATE.WHITE_LEDS] @white_leds.setter def white_leds(self, value): - """Not Implemented!""" telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_WHITE_LEDS) - utils.print_for_unimplemented_functions(Clue.white_leds.__name__) + self.__set_leds(CONSTANTS.CLUE_STATE.WHITE_LEDS, value) @property def red_led(self): - """Not Implemented! - - The red led next to the USB plug labeled LED. + """The red led next to the USB plug labeled LED. .. image :: ../docs/_static/red_led.jpg :alt: Red LED This example turns on the red LED. @@ -571,13 +570,12 @@ def red_led(self): clue.red_led = True """ telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_RED_LED) - utils.print_for_unimplemented_functions(Clue.red_led.__name__) + return self.__state[CONSTANTS.CLUE_STATE.RED_LED] @red_led.setter def red_led(self, value): - """Not Implemented!""" telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_RED_LED) - utils.print_for_unimplemented_functions(Clue.red_led.__name__) + self.__set_leds(CONSTANTS.CLUE_STATE.RED_LED, value) def play_tone(self, frequency, duration): """ Not Implemented! @@ -773,6 +771,12 @@ def __update_button(self, button, value): ) self.__state[button] = value + 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) + clue = Clue() # pylint: disable=invalid-name """Object that is automatically created on import. diff --git a/src/clue/test/test_adafruit_clue.py b/src/clue/test/test_adafruit_clue.py index 4ce4fcf50..0d7204205 100644 --- a/src/clue/test/test_adafruit_clue.py +++ b/src/clue/test/test_adafruit_clue.py @@ -175,3 +175,19 @@ def test_sea_level_pressure(self, mock_sea_level_pressure): def test_pixel(self, mock_color): clue.pixel.fill(mock_color) assert clue.pixel[0] == mock_color + + @pytest.mark.parametrize( + "value, expected", + [(True, True), (False, False), (1, True), ("a", True), (0, False), ("", False)], + ) + def test_red_led(self, value, expected): + clue.red_led = value + assert clue.red_led == expected + + @pytest.mark.parametrize( + "value, expected", + [(True, True), (False, False), (1, True), ("a", True), (0, False), ("", False)], + ) + def test_white_leds(self, value, expected): + clue.white_leds = value + assert clue.white_leds == expected diff --git a/src/view/components/clue/ClueImage.tsx b/src/view/components/clue/ClueImage.tsx index 7def6aa89..0ad426c96 100644 --- a/src/view/components/clue/ClueImage.tsx +++ b/src/view/components/clue/ClueImage.tsx @@ -16,7 +16,11 @@ interface EventTriggers { interface IProps { eventTriggers: EventTriggers; displayMessage: string; - neopixel: number[]; + leds: { + neopixel: number[]; + isRedLedOn: boolean; + isWhiteLedOn: boolean; + }; } export enum BUTTONS_KEYS { @@ -80,7 +84,7 @@ export class ClueImage extends React.Component { ); } diff --git a/src/view/components/clue/ClueSimulator.tsx b/src/view/components/clue/ClueSimulator.tsx index 61542d3a4..f0225d2e9 100644 --- a/src/view/components/clue/ClueSimulator.tsx +++ b/src/view/components/clue/ClueSimulator.tsx @@ -1,25 +1,29 @@ import * as React from "react"; import { AB_BUTTONS_KEYS, - // DEVICE_LIST_KEY, CONSTANTS, DEFAULT_IMG_CLUE, DEVICE_LIST_KEY, VIEW_STATE, WEBVIEW_MESSAGES, } from "../../constants"; -import { ViewStateContext } from "../../context"; 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 }, displayMessage: DEFAULT_IMG_CLUE, - neopixel: [0, 0, 0], + leds: { + neopixel: [0, 0, 0], + isRedLedOn: false, + isWhiteLedOn: false, + }, }; interface IState { @@ -34,7 +38,11 @@ interface IState { interface IClueState { buttons: { button_a: boolean; button_b: boolean }; displayMessage: string; - neopixel: number[]; + leds: { + neopixel: number[]; + isRedLedOn: boolean; + isWhiteLedOn: boolean; + }; } export class ClueSimulator extends React.Component { private imageRef: React.RefObject = React.createRef(); @@ -63,25 +71,7 @@ export class ClueSimulator extends React.Component { }); break; case "set-state": - console.log( - `message received ${JSON.stringify(message.state)}` - ); - if (message.state.display_base64) { - this.setState({ - clue: { - ...this.state.clue, - displayMessage: message.state.display_base64, - }, - }); - } else if (message.state.pixels) { - this.setState({ - clue: { - ...this.state.clue, - neopixel: message.state.pixels, - }, - }); - } - + this.handleStateChangeMessage(message); break; case "activate-play": const newRunningFile = this.state.currently_selected_file; @@ -142,7 +132,7 @@ export class ClueSimulator extends React.Component { onKeyEvent: this.onKeyEvent, }} displayMessage={this.state.clue.displayMessage} - neopixel={this.state.clue.neopixel} + leds={this.state.clue.leds} /> { this.refreshSimulatorClick(); } } + protected handleStateChangeMessage(message: any) { + if (message.state.display_base64 != null) { + this.setState({ + clue: { + ...this.state.clue, + displayMessage: message.state.display_base64, + }, + }); + } else if (message.state.pixels != null) { + this.setState({ + clue: { + ...this.state.clue, + leds: { + ...this.state.clue.leds, + neopixel: message.state.pixels, + }, + }, + }); + } else if (message.state.white_leds != null) { + this.setState({ + clue: { + ...this.state.clue, + leds: { + ...this.state.clue.leds, + isWhiteLedOn: message.state.white_leds, + }, + }, + }); + } else if (message.state.red_led != null) { + this.setState({ + clue: { + ...this.state.clue, + leds: { + ...this.state.clue.leds, + isRedLedOn: message.state.red_led, + }, + }, + }); + } + } } ClueSimulator.contextType = ViewStateContext; diff --git a/src/view/components/clue/Clue_svg.tsx b/src/view/components/clue/Clue_svg.tsx index 694e944c6..f5bfe5b92 100644 --- a/src/view/components/clue/Clue_svg.tsx +++ b/src/view/components/clue/Clue_svg.tsx @@ -2,22 +2,36 @@ // Licensed under the MIT license. import * as React from "react"; -import 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"; export interface IRefObject { [key: string]: React.RefObject; } interface IProps { displayImage: string; - neopixel: number[]; + leds: { + neopixel: number[]; + isRedLedOn: boolean; + isWhiteLedOn: boolean; + }; } export class ClueSvg extends React.Component { private svgRef: React.RefObject = React.createRef(); - private neopixel: React.RefObject = React.createRef(); - private pixelStopGradient: React.RefObject< - SVGStopElement - > = React.createRef(); + private ledsRefs = { + neopixel: React.createRef(), + redLed: React.createRef(), + whiteLeds: [ + React.createRef(), + React.createRef(), + ], + }; + private gradientRefs = { + neopixel: React.createRef(), + whiteLed: React.createRef(), + redLed: React.createRef(), + }; private buttonRefs: IRefObject = { BTN_A: React.createRef(), @@ -37,12 +51,10 @@ export class ClueSvg extends React.Component { return this.displayRef; } componentDidMount() { - this.updateDisplay(); - this.updateNeopixel(); + this.updateSvg(); } componentDidUpdate() { - this.updateDisplay(); - this.updateNeopixel(); + this.updateSvg(); } render() { @@ -59,7 +71,7 @@ export class ClueSvg extends React.Component { > { offset="0%" stopColor="rgb(0,0,0)" stopOpacity="1" - ref={this.pixelStopGradient} + ref={this.gradientRefs.neopixel} /> + + + + + + + + { transform="translate(-49.27 -48.48)" /> + { A+B + + + @@ -1031,15 +1093,35 @@ export class ClueSvg extends React.Component { rx="18.28" /> + + Red LED + + + Neopixel - - + + ); } + private updateSvg() { + this.updateDisplay(); + this.updateNeopixel(); + this.updateLeds(); + } private updateDisplay() { if (this.displayRef.current && this.props.displayImage) { @@ -1051,29 +1133,59 @@ export class ClueSvg extends React.Component { } private updateNeopixel() { - const { neopixel } = this.props; + const { neopixel } = this.props.leds; const rgbColor = `rgb(${neopixel[0] + (255 - neopixel[0]) * CONSTANTS.LED_TINT_FACTOR}, ${neopixel[1] + (255 - neopixel[1]) * CONSTANTS.LED_TINT_FACTOR},${neopixel[2] + (255 - neopixel[2]) * CONSTANTS.LED_TINT_FACTOR})`; - if (this.neopixel.current) { - this.neopixel.current.setAttribute("fill", rgbColor); + if (this.ledsRefs.neopixel.current) { + this.ledsRefs.neopixel.current.setAttribute("fill", rgbColor); } - if (this.pixelStopGradient.current) { - if (neopixel === DEFAULT_CLUE_STATE.neopixel) { - this.pixelStopGradient.current.setAttribute( + if (this.gradientRefs.neopixel.current) { + if (neopixel === DEFAULT_CLUE_STATE.leds.neopixel) { + this.gradientRefs.neopixel.current.setAttribute( "stop-opacity", "0" ); } else { - this.pixelStopGradient.current.setAttribute( + this.gradientRefs.neopixel.current.setAttribute( "stop-opacity", "1" ); } - this.pixelStopGradient.current.setAttribute("stop-color", rgbColor); + this.gradientRefs.neopixel.current.setAttribute( + "stop-color", + rgbColor + ); + } + } + private updateLeds() { + // update white led + const { isWhiteLedOn, isRedLedOn } = this.props.leds; + + 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 + ); + } + } + ); + 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 + ); } } } diff --git a/src/view/components/clue/__snapshots__/Clue.spec.tsx.snap b/src/view/components/clue/__snapshots__/Clue.spec.tsx.snap index 3f0368a07..812c851c8 100644 --- a/src/view/components/clue/__snapshots__/Clue.spec.tsx.snap +++ b/src/view/components/clue/__snapshots__/Clue.spec.tsx.snap @@ -30,7 +30,7 @@ Array [ cy="50%" fx="50%" fy="50%" - id="grad1" + id="gradNeopixel" r="70%" > + + + + + + + + A+B + + + + Red LED + + +