diff --git a/README.md b/README.md index e05930a..196f2d0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,111 @@ -# Readme +# Neuroscope -TODO +This project builds on [this boiler plate](https://github.com/a133xz/electron-vuejs-parcel-boilerplate) and adds functionality to control a Sphero Bolt using WebSocket commands. -Project builds on [this boiler plate](https://github.com/a133xz/electron-vuejs-parcel-boilerplate). +## Getting Started + +### Prerequisites + +- Node.js +- npm or yarn +- Python 3.x +- `websockets` Python package + +### Installation + +1. Clone the repository: + ```sh + git clone https://github.com/yourusername/neuroscope.git + cd neuroscope + ``` + +2. Install the dependencies: + ```sh + yarn install + ``` + +3. Start the Sphero server: + ```sh + python SpheroServer.py + ``` + +4. Start the Electron application: + ```sh + yarn serve + ``` + +## Usage + +### Controlling the Sphero + +The application sends WebSocket commands to control the Sphero Bolt. The following commands are available: + +- **drone-up**: Moves the Sphero up. +- **drone-down**: Moves the Sphero down. +- **drone-forward**: Moves the Sphero forward. + +### Code Changes + +The main changes are in the `index.js` file: + +1. **WebSocket Setup**: + ```javascript + const WebSocket = require('ws'); + const ws = new WebSocket('ws://localhost:8765'); + + ws.on('open', function open() { + console.log('WebSocket connection opened'); + }); + + ws.on('error', function error(err) { + console.error('WebSocket error:', err); + }); + ``` + +2. **Command Sending with Debounce**: + ```javascript + let lastCommandTime = 0; + const commandInterval = 3000; // 3 seconds + + function sendCommand(command) { + const currentTime = Date.now(); + if (currentTime - lastCommandTime >= commandInterval) { + ws.send(JSON.stringify(command)); + console.log("Command sent:", command); + lastCommandTime = currentTime; + } else { + console.log("Command skipped to avoid spamming:", command); + } + } + ``` + +3. **IPC Event Handlers**: + ```javascript + ipcMain.on("drone-up", (event, response) => { + let recent_val = parseInt(response); + let upVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + console.log("drone up", upVal, "sent", response); + const moveCommand = { action: "move", heading: 0, speed: upVal, duration: 1 }; + sendCommand(moveCommand); + }); + + ipcMain.on("drone-down", (event, response) => { + let recent_val = parseInt(response); + let downVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + console.log("drone down", downVal, "sent", response); + const moveCommand = { action: "move", heading: 180, speed: downVal, duration: 1 }; + sendCommand(moveCommand); + }); + + ipcMain.on("drone-forward", (event, response) => { + let recent_val = parseInt(response); + let forwardVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + console.log("drone forward", forwardVal, "sent", response); + const moveCommand = { action: "move", heading: 90, speed: forwardVal, duration: 1 }; + sendCommand(moveCommand); + }); + ``` + +## Work in Progress + +This project is a work in progress. The current implementation allows basic control of the Sphero Bolt using WebSocket commands. Further improvements and features are planned for future updates. diff --git a/SpheroServer.py b/SpheroServer.py new file mode 100644 index 0000000..032c0c3 --- /dev/null +++ b/SpheroServer.py @@ -0,0 +1,76 @@ +import asyncio +import websockets +import json +from spherov2 import scanner +from spherov2.sphero_edu import SpheroEduAPI +from spherov2.types import Color + +# Function to find the Sphero BOLT synchronously +async def find_toy(): + try: + print("Scanning for Sphero BOLT...") + # Wrapping the synchronous `find_toy` method in a coroutine + loop = asyncio.get_event_loop() + toy = await loop.run_in_executor(None, scanner.find_toy) + if not toy: + print("No Sphero BOLT found.") + return None + print("Sphero BOLT found!") + return toy + except Exception as e: + print(f"Error during toy scanning: {e}") + return None + +# Command handler for Sphero +async def handle_command(droid, command): + try: + if command["action"] == "led_on": + color = command.get("color", {"r": 0, "g": 255, "b": 0}) # Default green + print(f"Turning LED on with color: {color}") + droid.set_main_led(Color(r=color["r"], g=color["g"], b=color["b"])) + elif command["action"] == "led_off": + print("Turning LED off") + droid.set_main_led(Color(r=0, g=0, b=0)) # Turn off LED + elif command["action"] == "move": + print("Moving Sphero BOLT") + heading = command.get("heading", 0) # Default to up + speed = command.get("speed", 60) + duration = command.get("duration", 2) + droid.roll(heading, speed, duration) + else: + print(f"Unknown command: {command}") + except Exception as e: + print(f"Error handling command {command}: {e}") + raise e # Propagate the exception for better debugging + +async def handle_connection(websocket): + print("Client connected") + toy = await find_toy() # Find the Sphero BOLT asynchronously + if not toy: + print("Sphero BOLT not found!") + await websocket.send(json.dumps({"error": "Sphero BOLT not found!"})) + return + + with SpheroEduAPI(toy) as droid: + droid.set_main_led(Color(r=0, g=0, b=255)) # Set LED to blue for idle + try: + async for message in websocket: + print(f"Received message: {message}") + try: + command = json.loads(message) + await handle_command(droid, command) + except json.JSONDecodeError: + print(f"Invalid JSON received: {message}") + await websocket.send(json.dumps({"error": "Invalid JSON format"})) + except websockets.exceptions.ConnectionClosed: + print("Client disconnected") + except Exception as e: + print(f"Unexpected server error: {e}") + +async def main(): + print("Starting WebSocket server on ws://localhost:8765") + async with websockets.serve(handle_connection, "localhost", 8765): + await asyncio.Future() # Keep the server running indefinitely + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/SpheroTest.py b/SpheroTest.py new file mode 100644 index 0000000..5d29f06 --- /dev/null +++ b/SpheroTest.py @@ -0,0 +1,31 @@ +from spherov2 import scanner +from spherov2.sphero_edu import SpheroEduAPI +from spherov2.types import Color + +def main(): + print("Scanning for Sphero BOLT...") + toy = scanner.find_toy() # Synchronously find the Sphero BOLT + if not toy: + print("No Sphero BOLT found!") + return + + print("Sphero BOLT found! Connecting...") + with SpheroEduAPI(toy) as droid: + print("Connected to Sphero BOLT!") + + # Set LED color to green + print("Setting LED color to green...") + droid.set_main_led(Color(r=0, g=255, b=0)) + + # Move forward with heading 0, speed 60, for 2 seconds + print("Moving forward...") + droid.roll(heading=0, speed=60, duration=2) + + # Set LED color to red after moving + print("Setting LED color to red...") + droid.set_main_led(Color(r=255, g=0, b=0)) + + print("Done!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/main/index.js b/src/main/index.js index c2a3429..84ca2b2 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,6 +1,7 @@ const { app, BrowserWindow, ipcMain, dialog } = require("electron"); const path = require("path"); const tello = require("./tello.js"); +const WebSocket = require('ws'); const isProduction = process.env.NODE_ENV === "production" || !process || !process.env || !process.env.NODE_ENV; @@ -56,7 +57,7 @@ async function createWindow() { // Reload try { require("electron-reloader")(module); - } catch (_) {} + } catch (_) { } // Errors are thrown if the dev tools are opened // before the DOM is ready win.webContents.once("dom-ready", async () => { @@ -125,33 +126,63 @@ async function createWindow() { } }); + const ws = new WebSocket('ws://localhost:8765'); + + ws.on('open', function open() { + console.log('WebSocket connection opened'); + }); + + ws.on('error', function error(err) { + console.error('WebSocket error:', err); + }); + + let lastCommandTime = 0; + const commandInterval = 3000; // 3 seconds + + function sendCommand(command) { + const currentTime = Date.now(); + if (currentTime - lastCommandTime >= commandInterval) { + ws.send(JSON.stringify(command)); + console.log("Command sent:", command); + lastCommandTime = currentTime; + } else { + console.log("Command skipped to avoid spamming:", command); + } + } + ipcMain.on("drone-up", (event, response) => { let recent_val = parseInt(response); - let upVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; - console.log("drone up", upVal, "sent", response); - tello.up(upVal); + let rightVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + console.log("Sphero right", rightVal, "sent", response); + const moveCommand = { action: "move", heading: 90, speed: rightVal, duration: 3 }; + sendCommand(moveCommand); }); ipcMain.on("drone-down", (event, response) => { let recent_val = parseInt(response); let downVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; - console.log("drone down", downVal, "sent", response); - tello.down(downVal); + console.log("Sphero Left", downVal, "sent", response); + const moveCommand = { action: "move", heading: 270, speed: downVal, duration: 3 }; + sendCommand(moveCommand); }); ipcMain.on("drone-forward", (event, response) => { let recent_val = parseInt(response); - _maxSpeed = 80; - let val = recent_val > _maxSpeed ? _maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; - console.log("drone forward", val, "sent", response); - tello.forward(val); + let forwardVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + console.log("drone forward", forwardVal, "sent", response); + const moveCommand = { action: "move", heading: 0, speed: forwardVal, duration: 1 }; + sendCommand(moveCommand); }); ipcMain.on("drone-back", (event, response) => { let recent_val = parseInt(response); - let val = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; - console.log("drone back", val, "sent", response); - tello.back(val); + let backVal = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + console.log("drone back", backVal, "sent", response); + // let val = recent_val > maxSpeed ? maxSpeed : recent_val < minSpeed ? minSpeed : recent_val; + // console.log("drone back", val, "sent", response); + const moveCommand = { action: "move", heading: 180, speed: backVal, duration: 1 }; + sendCommand(moveCommand); + // tello.back(val); }); ipcMain.on("cw", (event, response) => { diff --git a/src/renderer/js/customblock.js b/src/renderer/js/customblock.js index ab26ff1..9d4b98c 100644 --- a/src/renderer/js/customblock.js +++ b/src/renderer/js/customblock.js @@ -212,7 +212,7 @@ javascriptGenerator.forBlock["wait_seconds"] = function (block) { /* droneUp() */ var droneUp = { type: "drone_up", - message0: "up %1 cm", + message0: "Right %1 cm", args0: [{ type: "input_value", name: "value", check: "Number" }], previousStatement: null, nextStatement: null, @@ -236,7 +236,7 @@ javascriptGenerator.forBlock["drone_up"] = function (block, generator) { /* droneDown() */ var droneDown = { type: "drone_down", - message0: "down %1 cm", + message0: "Left %1 cm", args0: [{ type: "input_value", name: "value", check: "Number" }], previousStatement: null, nextStatement: null,