Skip to content
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<IP_OR_BOARD_NAME>:7000` address
- Open the Scratch interface at the `https://<IP_OR_BOARD_NAME>: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://<YOUR_IP>:7000`;` in the `index.js`
- change the `const DEFAULT_HOST =`<YOUR_IP|BOARD_NAME>`;` in the `scratch-arduino-extensions/packages/scratch-vm/src/extensions/ArduinoUnoQ.js`
- Open local scratch on http://localhost:8601/
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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();
}
}

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -37,6 +25,9 @@ class ArduinoObjectDetection {
constructor(runtime) {
this.runtime = runtime;

this.unoq = new ArduinoUnoQ();
this.unoq.connect();

/** @type {Array<Detection>} */
this.detectedObjects = [];

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand Down