diff --git a/README.md b/README.md index a025a5e..17f42a0 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,21 @@ Accessible from any device via a browser, it makes coding, electronics, and AI h ## Installation -- Connect the Arduino Uno Q board via USB -- Open an `adb shell` into the board using [adb](https://docs.arduino.cc/software/app-lab/tutorials/cli/) -- Copy and paste the following command into the terminal to install the latest `scratch-arduino-app` into the board: +- Open a terminal inside the UNO Q board (you can also use the [adb](https://docs.arduino.cc/software/app-lab/tutorials/cli/) tool) +- Copy and paste the following command into the terminal to install the latest `scratch-arduino-app`: ``` curl -sSL https://raw.githubusercontent.com/dido18/scratch-arduino-app/main/install.sh | bash ``` -- Open the Scratch interface at the `:7000` address +- Open the Scratch interface at the `https://:7000` address. -### Local development +NOTE: the `https` is needed by the `getUserMedia()` method for security reason. + +## Local development - `task scratch:init` - `task scratch:local:start` - `ŧask board:upload` -- change the `const wsServerURL =`ws://:7000`;` in the `index.js` +- change the `const DEFAULT_HOST =``;` in the `scratch-arduino-extensions/packages/scratch-vm/src/extensions/ArduinoUnoQ.js` - Open local scratch on http://localhost:8601/ diff --git a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/ArduinoUnoQ.js b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/ArduinoUnoQ.js new file mode 100644 index 0000000..734401c --- /dev/null +++ b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/ArduinoUnoQ.js @@ -0,0 +1,133 @@ +const io = require("./socket.io.min.js"); + +const DEFAULT_HOST = window.location.hostname; + +class ArduinoUnoQ { + constructor() { + this.serverURL = `wss://${DEFAULT_HOST}:7000`; + + this.io = io(this.serverURL, { + path: "/socket.io", + transports: ["polling", "websocket"], + autoConnect: true, + }); + this.isConnected = false; + + this._setupConnectionHandlers(); + } + + on(event, callback) { + if (this.io) { + this.io.on(event, callback); + console.log(`Registered event listener for: ${event}`); + } else { + console.error("Socket.io not initialized"); + } + } + + emit(event, data) { + if (this.io && this.isConnected) { + this.io.emit(event, data); + console.log(`Emitted event: ${event}`, data); + } else { + console.warn(`Cannot emit ${event}: Not connected to Arduino UNO Q`); + } + } + + _setupConnectionHandlers() { + this.io.on("connect", () => { + this.isConnected = true; + console.log(`Connected to Arduino UNO Q at ${this.serverURL}`); + }); + + this.io.on("disconnect", (reason) => { + this.isConnected = false; + console.log(`Disconnected from Arduino UNO Q: ${reason}`); + }); + + this.io.on("connect_error", (error) => { + console.error(`Connection error:`, error.message); + }); + + this.io.on("reconnect", (attemptNumber) => { + console.log(`Reconnected to Arduino UNO Q after ${attemptNumber} attempts`); + }); + } + + connect() { + if (!this.io.connected) { + console.log("Attempting to connect to Arduino UNO Q..."); + this.io.connect(); + } + } + + disconnect() { + if (this.io.connected) { + console.log("Disconnecting from Arduino UNO Q..."); + this.io.disconnect(); + } + } + + // ===== LED CONTROL METHODS ===== + /** + * Set RGB LED color + * @param {string} led - LED identifier ("LED3" or "LED4") + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + */ + setLedRGB(led, r, g, b) { + this.io.emit("set_led_rgb", { + led: led, + r: Math.max(0, Math.min(255, r)), + g: Math.max(0, Math.min(255, g)), + b: Math.max(0, Math.min(255, b)), + }); + console.log(`Setting ${led} to RGB(${r}, ${g}, ${b})`); + } + + /** + * Turn off LED + * @param {string} led - LED identifier ("LED3" or "LED4") + */ + turnOffLed(led) { + this.setLedRGB(led, 0, 0, 0); + } + + // ===== MATRIX CONTROL METHODS ===== + + /** + * Draw frame on LED matrix + * @param {string} frame - 25-character string representing 5x5 matrix (0s and 1s) + */ + matrixDraw(frame) { + if (typeof frame !== "string" || frame.length !== 25) { + console.error("Invalid frame format. Expected 25-character string of 0s and 1s"); + return; + } + // Validate frame contains only 0s and 1s + if (!/^[01]+$/.test(frame)) { + console.error("Frame must contain only 0s and 1s"); + return; + } + + this.io.emit("matrix_draw", { frame: frame }); + console.log(`Drawing matrix frame: ${frame}`); + } + + matrixClear() { + const clearFrame = "0".repeat(25); + this.matrixDraw(clearFrame); + } + + // AI object detection + + detectObjects(imageData) { + this.io.emit("detect_objects", { image: imageData }); + console.log("Emitted detect_objects event"); + } + + // ===== EVENT HANDLING METHODS ===== +} + +module.exports = ArduinoUnoQ; diff --git a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_basics/index.js b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_basics/index.js index d6431a5..8f95ab3 100644 --- a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_basics/index.js +++ b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_basics/index.js @@ -1,35 +1,18 @@ -// const formatMessage = require('../../../../../../scratch-editor/node_modules/format-message'); const BlockType = require("../../../../../../scratch-editor/packages/scratch-vm/src/extension-support/block-type"); const ArgumentType = require( "../../../../../../scratch-editor/packages/scratch-vm/src/extension-support/argument-type", ); -const io = require("../socket.io.min.js"); +const ArduinoUnoQ = require("../ArduinoUnoQ"); -/** - * Url of icon to be displayed at the left edge of each extension block. - * @type {string} - */ -// eslint-disable-next-line max-len +// TODO: add icons const iconURI = ""; - -/** - * Url of icon to be displayed in the toolbox menu for the extension category. - * @type {string} - */ -// eslint-disable-next-line max-len const menuIconURI = ""; -const wsServerURL = `${window.location.protocol}//${window.location.hostname}:7000`; - class ArduinoBasics { constructor(runtime) { this.runtime = runtime; - - this.io = io(wsServerURL, { - path: "/socket.io", - transports: ["polling", "websocket"], - autoConnect: true, - }); + this.unoq = new ArduinoUnoQ(); + this.unoq.connect(); } } @@ -88,27 +71,23 @@ ArduinoBasics.prototype.getInfo = function() { }; ArduinoBasics.prototype.matrixDraw = function(args) { - console.log(`Drawing frame on matrix: ${args}`); - this.io.emit("matrix_draw", { frame: args.FRAME }); + this.unoq.matrixDraw(args.FRAME); }; ArduinoBasics.prototype.matrixClear = function() { - console.log("Clearing matrix"); - this.io.emit("matrix_draw", { frame: "0000000000000000000000000" }); + this.unoq.matrixClear(); }; ArduinoBasics.prototype.setLed3 = function(args) { const hexColor = args.HEX; const rgb = this.hexToRgb(hexColor); - console.log(`Setting led 3 to: r:${rgb.r}, g:${rgb.g}, b:${rgb.b} (HEX: ${hexColor})`); - this.io.emit("set_led_rgb", { led: "LED3", r: rgb.r, g: rgb.g, b: rgb.b }); + this.unoq.setLedRGB("LED3", rgb.r, rgb.g, rgb.b); }; ArduinoBasics.prototype.setLed4 = function(args) { const hexColor = args.HEX; const rgb = this.hexToRgb(hexColor); - console.log(`Setting led 4 to: r:${rgb.r}, g:${rgb.g}, b:${rgb.b} (HEX: ${hexColor})`); - this.io.emit("set_led_rgb", { led: "LED4", r: rgb.r, g: rgb.g, b: rgb.b }); + this.unoq.setLedRGB("LED4", rgb.r, rgb.g, rgb.b); }; ArduinoBasics.prototype.hexToRgb = function(hex) { diff --git a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_modulino/index.js b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_modulino/index.js index 4b3fb5a..572a0c0 100644 --- a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_modulino/index.js +++ b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_modulino/index.js @@ -2,36 +2,21 @@ const BlockType = require("../../../../../../scratch-editor/packages/scratch-vm/ const ArgumentType = require( "../../../../../../scratch-editor/packages/scratch-vm/src/extension-support/argument-type", ); -const io = require("../socket.io.min.js"); +const ArduinoUnoQ = require("../ArduinoUnoQ"); -/** - * Url of icon to be displayed at the left edge of each extension block. - * @type {string} - */ -// eslint-disable-next-line max-len +// TODO: add icons const iconURI = ""; - -/** - * Url of icon to be displayed in the toolbox menu for the extension category. - * @type {string} - */ -// eslint-disable-next-line max-len const menuIconURI = ""; -const wsServerURL = `${window.location.protocol}//${window.location.hostname}:7000`; - class ArduinoModulino { constructor(runtime) { this.runtime = runtime; - this.io = io(wsServerURL, { - path: "/socket.io", - transports: ["polling", "websocket"], - autoConnect: true, - }); + this.unoq = new ArduinoUnoQ(); + this.unoq.connect(); // TODO: move to ModulinoPeripheral this._button_pressed = ""; - this.io.on("modulino_buttons_pressed", (data) => { + this.unoq.on("modulino_buttons_pressed", (data) => { console.log(`Modulino button pressed event received: ${data.btn}`); this._button_pressed = data.btn.toUpperCase(); }); diff --git a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_object_detection/index.js b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_object_detection/index.js index 29dac29..66d1897 100644 --- a/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_object_detection/index.js +++ b/scratch-arduino-extensions/packages/scratch-vm/src/extensions/arduino_object_detection/index.js @@ -2,28 +2,16 @@ const BlockType = require("../../../../../../scratch-editor/packages/scratch-vm/ const ArgumentType = require( "../../../../../../scratch-editor/packages/scratch-vm/src/extension-support/argument-type", ); -const io = require("../socket.io.min.js"); const Video = require("../../../../../../scratch-editor/packages/scratch-vm/src/io/video"); const Rectangle = require("../../../../../../scratch-editor/packages/scratch-render/src/Rectangle.js"); const StageLayering = require("../../../../../../scratch-editor/packages/scratch-vm/src/engine/stage-layering.js"); const { Detection, MODEL_LABELS } = require("./object_detection"); +const ArduinoUnoQ = require("../ArduinoUnoQ"); -/** - * Url of icon to be displayed at the left edge of each extension block. - * @type {string} - */ -// eslint-disable-next-line max-len +// TODO add icons const iconURI = ""; - -/** - * Url of icon to be displayed in the toolbox menu for the extension category. - * @type {string} - */ -// eslint-disable-next-line max-len const menuIconURI = ""; -const wsServerURL = `${window.location.protocol}//${window.location.hostname}:7000`; - /** * RGB color constants for confidence visualization */ @@ -37,6 +25,9 @@ class ArduinoObjectDetection { constructor(runtime) { this.runtime = runtime; + this.unoq = new ArduinoUnoQ(); + this.unoq.connect(); + /** @type {Array} */ this.detectedObjects = []; @@ -66,15 +57,8 @@ class ArduinoObjectDetection { } }); - this.io = io(wsServerURL, { - path: "/socket.io", - transports: ["polling", "websocket"], - autoConnect: true, - }); - - this.io.on("detection_result", (data) => { + this.unoq.on("detection_result", (data) => { this.detectedObjects = []; - this._clearBoundingBoxes(); data.detection.forEach((detection) => { @@ -264,7 +248,7 @@ ArduinoObjectDetection.prototype._detectObjects = function(args) { } const dataUrl = canvas.toDataURL("image/png"); const base64Frame = dataUrl.split(",")[1]; - this.io.emit("detect_objects", { image: base64Frame }); + this.unoq.detectObjects(base64Frame); }; ArduinoObjectDetection.prototype._clearBoundingBoxes = function(args) {