diff --git a/demo/Joystick Example/README.md b/demo/Joystick Example/README.md index 23e51de..df6b4a3 100644 --- a/demo/Joystick Example/README.md +++ b/demo/Joystick Example/README.md @@ -1,27 +1,16 @@ -# 🕹️ Qwiic Joystick Asteroids +# Qwiic Joystick Example -Stream the SparkFun Qwiic Joystick's X/Y position (and button) over serial, then **play a -full game of Asteroids** with it in the **Visual** tab. +Stream the SparkFun Qwiic Joystick's X/Y position (and button) over serial, then watch +it move on a live grid in the **Visual** tab. Built with **tinyStudio** for the **tinyCore** ESP32-S3 board by MR.INDUSTRIES. ---- - ## What it does - [x] Talks to the Qwiic Joystick over I²C (default address `0x20`) - [x] Reads horizontal + vertical as 10-bit values (`0..1023`, center `~512`) -- [x] Streams `x,y,button` over serial as a simple CSV line -- [x] `visual.js` parses that stream and lets you **fly a ship, rotate, thrust, and shoot rocks** - -```mermaid -flowchart LR - JOY[Qwiic Joystick] -->|I²C SDA/SCL| MCU[tinyCore] - MCU -->|"Serial.println x,y,button"| MON[Serial Stream] - MON --> VIS[visual.js · Asteroids] -``` - ---- +- [x] Streams `x,y,button` over serial at `115200` baud +- [x] `visual.js` plots the position on an X/Y grid to match ## Hardware @@ -31,115 +20,27 @@ soldering, no breadboard. (Qwiic carries `3V3 · GND · SDA · SCL`.) Install the **SparkFun Qwiic Joystick Arduino Library** from the Library Manager. ---- - -## Serial format - -One CSV line per sample, ~20 Hz: - -``` -512,498,0 -``` - -`x , y , button` — where `x`/`y` are `0..1023` (center `~512`) and `button` is `1` while pressed. - ---- - -## 📟 `qwiic_joystick.ino` — the firmware - -This is the code that runs on the tinyCore. Its only job is to read the joystick and print -a clean CSV line that the visualizer can parse. - -### Walkthrough - -| Stage | What happens | -|-------|--------------| -| **Includes** | `Wire.h` for I²C and `SparkFun_Qwiic_Joystick_Arduino_Library.h` for the driver. A global `JOYSTICK joystick;` object handles all register reads. | -| **`setup()`** | Starts serial at `9600` baud, then brings up the I²C bus with `Wire.begin(3, 4)` — pins **3 (SDA)** and **4 (SCL)** are the tinyCore's Qwiic bus. | -| **Self-check** | `joystick.begin()` returns `false` if the device isn't found. If so, it prints a hint to check the Qwiic cable and halts in a `while (1)` loop so you're not left guessing. | -| **`loop()`** | Reads the three values and prints them as `x,y,button`, then `delay(50)` paces the stream to roughly 20 Hz. | +## Signal flow -### Key lines - -```cpp -int x = joystick.getHorizontal(); // 0..1023, center ~512 -int y = joystick.getVertical(); // 0..1023, center ~512 -int b = (joystick.getButton() == 0) ? 1 : 0; // active-low → 1 = pressed +```mermaid +flowchart LR + JOY[Qwiic Joystick] -->|I²C SDA/SCL| MCU[tinyCore] + MCU -->|Serial.println x,y,button| MON[Serial Monitor] + MON --> VIS[visual.js grid] ``` -> **Note:** the joystick button is **active-low** (the library returns `0` when pressed), -> so the sketch inverts it to the more intuitive `1 = pressed` before sending. - ---- - -## 🎮 `visual.js` — the Asteroids game - -A p5.js sketch that turns the joystick stream into a playable game. It reads the **same CSV -format** the firmware emits, so the firmware and visualizer stay perfectly in sync. - -### Controls - -| Action | Joystick | Keyboard fallback* | -|--------|----------|--------------------| -| Rotate | Tilt **left / right** | ← / → arrows | -| Thrust | Push **up** | ↑ arrow | -| Fire | Press the **button** | Spacebar | -| Restart (after game over) | Press the **button** | Spacebar | - -> \*If the tinyStudio serial harness isn't present (e.g. a plain browser), `visual.js` -> automatically falls back to the keyboard so you can test the game before wiring anything up. - -### How the input is read +## Serial format -```js -// parse "x,y,button" -> {x, y, b} (same regex the firmware feeds) -function parseLine(s) { ... } +One line per sample, ~20 Hz: -// pull the latest line from the harness, or null when not connected -function readJoystick() { - if (typeof serialRead !== 'function') return null; - return parseLine(serialRead()); -} ``` - -Each frame `handleInput()` grabs the newest joystick reading. A **deadzone** (`±90` around -center) ignores small wobble so the ship doesn't drift at rest, and a **rising-edge** check -on the button (`btn === 1 && lastBtn === 0`) means one press fires exactly one shot. - -### Game architecture - -| Piece | Role | -|-------|------| -| **`ship`** | Position, velocity, and angle. X tilt rotates it; pushing up adds thrust along its heading. Movement uses friction + a max-speed clamp, and the ship **wraps** around screen edges. | -| **`bullets`** | Spawned from the ship's nose, inherit ship velocity, and expire after `BULLET_LIFE` frames. | -| **`rocks`** | Lumpy procedurally-generated asteroids in three sizes. Large rocks split into two smaller ones when shot; small rocks just vanish. | -| **`checkCollisions()`** | Bullet-vs-rock (score + split) and ship-vs-rock (lose a life). Smaller rocks are worth more points. | -| **State** | `'play'` / `'over'`, with `lives`, `score`, a fire cooldown, and brief **invulnerability** (the blinking ship) after respawning. | -| **`nextWave()`** | Clearing all rocks spawns a new, slightly larger wave that scales with your score. | - -### Tuning knobs - -All the feel of the game lives in a block of constants near the top, so it's easy to tweak: - -```js -const DEADZONE = 90; // ignore small wobble around center -const TURN_SPEED = 0.07; // radians per frame at full tilt -const THRUST_ACCEL = 0.12; -const FRICTION = 0.99; -const MAX_SPEED = 7; -const BULLET_SPEED = 7; -const FIRE_COOLDOWN = 10; // frames between shots -const START_LIVES = 3; +512,498,0 ``` -The palette constants (`C_BG`, `C_SHIP`, `C_ROCK`, `C_BULLET`) keep the firmware demo and the -game looking consistent. - ---- +`x , y , button` — `button` is `1` while pressed. -## Build & play +## Build -1. **Verify** to compile the sketch. +1. **Verify** to compile. 2. **Upload** to your connected tinyCore. -3. Open the **Visual** tab — tilt to steer, push up to thrust, and press the button to shoot. -4. Clear every rock to advance to the next wave. Good luck! 🚀 +3. Open the **Visual** tab to watch the joystick move on the grid. diff --git a/demo/Joystick Example/qwiic_joystick/qwiic_joystick.ino b/demo/Joystick Example/qwiic_joystick/qwiic_joystick.ino index 4649e3f..1afaedc 100644 --- a/demo/Joystick Example/qwiic_joystick/qwiic_joystick.ino +++ b/demo/Joystick Example/qwiic_joystick/qwiic_joystick.ino @@ -11,10 +11,8 @@ JOYSTICK joystick; void setup() { - Serial.begin(9600); - Serial.println("Hello World"); + Serial.begin(115200); - //Wire.begin(); Wire.begin(3, 4); // tinyCore Qwiic bus = I2C (SDA/SCL) delay(100); diff --git a/demo/Joystick Example/visual.js b/demo/Joystick Example/visual.js index 19e2216..85b7c6e 100644 --- a/demo/Joystick Example/visual.js +++ b/demo/Joystick Example/visual.js @@ -1,102 +1,233 @@ -// visual.js — Qwiic Joystick X/Y grid visualizer -// Plots the joystick's live position on a grid to match what the sketch streams. -// The sketch prints one CSV line per sample: x,y,button (x,y in 0..1023) +// pong.js — Qwiic Joystick Pong +// Single-player Pong driven by the SparkFun Qwiic Joystick. +// Reads the same CSV stream as the visualizer: x,y,button (x,y in 0..1023) +// Push the stick UP / DOWN to move the left paddle. Press the button to serve. // // serialRead() -> latest raw line, e.g. "512,498,0" -// serialLines() -> recent raw lines (used to draw the trail) +// serialLines() -> recent raw lines (unused here, kept for parity) +// serialEvent() -> fires per new line (kept for parity) +// +// Built for tinyStudio's harness. If the serial functions aren't present +// (e.g. a plain browser / the p5 web editor), it falls back to the mouse so +// you can test the game logic before wiring up hardware. + +const RANGE = 1023; // 10-bit ADC full scale +const WIN_SCORE = 7; + +// ---- palette (matches the visualizer) ---- +const C_BG = [10, 15, 45]; +const C_NET = [26, 34, 70]; +const C_PLAYER = [255, 255, 255]; // cyan +const C_AI = [255, 255, 255]; // pink +const C_BALL = [255, 215, 0]; // gold +const C_TEXT = [150, 162, 196]; -const RANGE = 1023; // 10-bit ADC full scale -const PAD = 46; // grid margin -let px = 512, py = 512; // last good position (rest = centered) -let pressed = false; +// ---- geometry ---- +const PADDLE_W = 12; +const PADDLE_H = 84; +const PADDLE_INSET = 26; // distance of the paddle face from the wall +const BALL_R = 9; +const AI_MAX_SPEED = 5.2; // lower = easier to beat + +let player, ai, ball; +let scoreP = 0, scoreA = 0; +let state = 'serve'; // 'serve' | 'play' | 'over' +let winner = ''; +let joyY = 512, lastBtn = 0; function setup() { - createCanvas(440, 440); + createCanvas(640, 440); textFont('monospace'); + player = { x: PADDLE_INSET, y: height / 2 }; + ai = { x: width - PADDLE_INSET, y: height / 2 }; + resetBall(1); } -// parse "x,y,button" -> {x,y,b} or null if the line has no numbers +// parse "x,y,button" -> {x,y,b} or null (same regex as the visualizer) function parseLine(s) { let m = String(s).match(/(-?\d+)\s*,\s*(-?\d+)(?:\s*,\s*(\d+))?/); if (!m) return null; return { x: int(m[1]), y: int(m[2]), b: m[3] ? int(m[3]) : 0 }; } -// joystick -> screen. Physical stick grows LEFT in X and UP in Y -// (matching the SparkFun hookup directions), so X is inverted. -function sx(x) { return map(x, RANGE, 0, PAD, width - PAD); } -function sy(y) { return map(y, 0, RANGE, height - PAD, PAD); } +// read the joystick if the harness is present, else null +function readJoystick() { + if (typeof serialRead !== 'function') return null; + return parseLine(serialRead()); +} + +function resetBall(dir) { + // dir: +1 serves right (toward CPU), -1 serves left (toward you) + ball = { + x: width / 2, + y: height / 2, + vx: 4.2 * dir, + vy: random(-2.2, 2.2), + speed: 4.6 + }; +} function draw() { - background(10, 15, 45); - - let p = parseLine(serialRead()); - if (p) { px = p.x; py = p.y; pressed = p.b === 1; } - - drawGrid(); - - // trail of recent samples - let lines = serialLines(); - noFill(); - stroke(0, 240, 255, 90); - strokeWeight(2); - beginShape(); - for (let i = max(0, lines.length - 60); i < lines.length; i++) { - let q = parseLine(lines[i]); - if (q) vertex(sx(q.x), sy(q.y)); + background(C_BG); + handleInput(); + if (state === 'play') updateBall(); + drawNet(); + drawPaddles(); + drawBall(); + drawHud(); +} + +function handleInput() { + let p = readJoystick(); + let btn = 0; + + if (p) { + joyY = p.y; // fresh joystick sample + btn = p.b; + } else if (typeof serialRead !== 'function') { + // no harness: mouse Y drives the paddle, click serves + joyY = map(mouseY, height, 0, 0, RANGE, true); + btn = mouseIsPressed ? 1 : 0; } - endShape(); + // (harness present but no parseable line yet -> keep last joyY, btn stays 0) - let gx = sx(px), gy = sy(py); + // stick UP -> paddle UP, matching the visualizer's inverted Y + player.y = map(joyY, 0, RANGE, height - PADDLE_H / 2, PADDLE_H / 2, true); - // crosshair through the live point - stroke(38, 48, 92); - strokeWeight(1); - line(PAD, gy, width - PAD, gy); - line(gx, PAD, gx, height - PAD); + // rising-edge button: serve or restart + if (btn === 1 && lastBtn === 0) onButton(); + lastBtn = btn; +} - // the joystick dot (glows gold while the button is pressed) - noStroke(); - let r = pressed ? 26 : 18; - fill(255, 63, 140, 60); - circle(gx, gy, r + 16); - fill(pressed ? color(255, 215, 0) : color(255, 63, 140)); - circle(gx, gy, r); +function onButton() { + if (state === 'serve') { + state = 'play'; + } else if (state === 'over') { + scoreP = 0; + scoreA = 0; + winner = ''; + state = 'serve'; + resetBall(random() < 0.5 ? 1 : -1); + } +} + +function updateBall() { + ball.x += ball.vx; + ball.y += ball.vy; + + // top / bottom walls + if (ball.y - BALL_R < 0) { ball.y = BALL_R; ball.vy *= -1; } + if (ball.y + BALL_R > height) { ball.y = height - BALL_R; ball.vy *= -1; } + + bouncePaddle(player, +1); // left paddle -> ball leaves rightward + bouncePaddle(ai, -1); // right paddle -> ball leaves leftward + + // CPU: chase only while the ball approaches, else drift back to center + let targetY = ball.vx > 0 ? ball.y : height / 2; + ai.y += constrain(targetY - ai.y, -AI_MAX_SPEED, AI_MAX_SPEED); + ai.y = constrain(ai.y, PADDLE_H / 2, height - PADDLE_H / 2); + + // scoring + if (ball.x + BALL_R < 0) { scoreA++; afterPoint(-1); } // CPU scored + if (ball.x - BALL_R > width) { scoreP++; afterPoint(+1); } // you scored +} + +function bouncePaddle(pad, dir) { + // dir: +1 = paddle on the left, -1 = paddle on the right + let halfW = PADDLE_W / 2; + let halfH = PADDLE_H / 2; + + // only register a hit when the ball is heading toward this paddle + if (dir > 0 && ball.vx >= 0) return; + if (dir < 0 && ball.vx <= 0) return; + + let withinX = abs(ball.x - pad.x) <= halfW + BALL_R; + let withinY = ball.y >= pad.y - halfH - BALL_R && + ball.y <= pad.y + halfH + BALL_R; + if (!(withinX && withinY)) return; + + // reflect with angle based on where it struck the paddle, speed up a touch + ball.speed = min(ball.speed + 0.35, 11); + let offset = constrain((ball.y - pad.y) / halfH, -1, 1); // -1 (top) .. 1 (bottom) + let angle = offset * (PI / 3.2); // up to ~56° + ball.vx = ball.speed * cos(angle) * dir; + ball.vy = ball.speed * sin(angle); - drawReadout(gx, gy); + // nudge clear so it can't stick inside the paddle + ball.x = pad.x + dir * (halfW + BALL_R + 0.5); } -function drawGrid() { - stroke(26, 34, 70); - strokeWeight(1); - const cells = 8; - for (let i = 0; i <= cells; i++) { - let x = map(i, 0, cells, PAD, width - PAD); - let y = map(i, 0, cells, PAD, height - PAD); - line(x, PAD, x, height - PAD); - line(PAD, y, width - PAD, y); +function afterPoint(scorer) { + // scorer: +1 you scored, -1 CPU scored + if (scoreP >= WIN_SCORE || scoreA >= WIN_SCORE) { + state = 'over'; + winner = scoreP > scoreA ? 'PLAYER' : 'CPU'; + return; } - // center cross — the ~512,512 rest position - stroke(58, 70, 120); - strokeWeight(1.5); - let cx = sx(512), cy = sy(512); - line(cx - 8, cy, cx + 8, cy); - line(cx, cy - 8, cx, cy + 8); + state = 'serve'; + resetBall(scorer); // serve toward whoever just conceded the point +} + +function drawNet() { + stroke(C_NET); + strokeWeight(3); + for (let y = 12; y < height; y += 26) line(width / 2, y, width / 2, y + 14); +} + +function drawPaddles() { + noStroke(); + rectMode(CENTER); + // player (cyan) with glow + fill(C_PLAYER[0], C_PLAYER[1], C_PLAYER[2], 60); + rect(player.x, player.y, PADDLE_W + 8, PADDLE_H + 8, 6); + fill(C_PLAYER); + rect(player.x, player.y, PADDLE_W, PADDLE_H, 4); + // CPU (pink) with glow + fill(C_AI[0], C_AI[1], C_AI[2], 60); + rect(ai.x, ai.y, PADDLE_W + 8, PADDLE_H + 8, 6); + fill(C_AI); + rect(ai.x, ai.y, PADDLE_W, PADDLE_H, 4); + rectMode(CORNER); } -function drawReadout() { +function drawBall() { noStroke(); - textSize(12); - fill(150, 162, 196); + fill(C_BALL[0], C_BALL[1], C_BALL[2], 60); + circle(ball.x, ball.y, BALL_R * 2 + 12); + fill(C_BALL); + circle(ball.x, ball.y, BALL_R * 2); +} + +function drawHud() { + noStroke(); + textAlign(CENTER, TOP); + textSize(34); + fill(C_PLAYER); + text(scoreP, width / 2 - 60, 12); + fill(C_AI); + text(scoreA, width / 2 + 60, 12); + + textSize(11); + fill(C_TEXT); textAlign(LEFT, TOP); - text('Qwiic Joystick', 12, 12); - fill(0, 240, 255); + text('YOU', 14, 16); textAlign(RIGHT, TOP); - text('X ' + nf(px, 4) + ' Y ' + nf(py, 4), width - 12, 12); - fill(pressed ? color(255, 215, 0) : color(90, 100, 140)); - textAlign(RIGHT, BOTTOM); - text(pressed ? '\u25CF BUTTON' : '\u25CB button', width - 12, height - 12); + text('CPU', width - 14, 16); + + if (state === 'serve') { + banner('PRESS BUTTON TO SERVE'); + } else if (state === 'over') { + banner((winner === 'PLAYER' ? 'YOU WIN!' : 'CPU WINS') + ' PRESS TO RESTART'); + } +} + +function banner(msg) { + noStroke(); + textAlign(CENTER, CENTER); + textSize(16); + fill(C_BALL); + text(msg, width / 2, height - 28); } -// fires on every new serial line (kept for parity with the harness) +// fires per new serial line (kept for parity with the harness) function serialEvent(line) {} \ No newline at end of file diff --git a/package.json b/package.json index 7a7baa7..9409132 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tinystudio", - "version": "0.1.0", + "version": "0.1.1", "description": "An open-source desktop + web IDE for building, wiring, simulating, and flashing tiny hardware projects — code, circuit, and visual views in one place.", "main": "./out/main/index.js", "author": "MR.INDUSTRIES", diff --git a/src/renderer/src/commands/fileCommands.ts b/src/renderer/src/commands/fileCommands.ts index c1a786a..b7d97f4 100644 --- a/src/renderer/src/commands/fileCommands.ts +++ b/src/renderer/src/commands/fileCommands.ts @@ -281,6 +281,22 @@ export class LoadGitHubProjectCommand implements Command { ) } + const name = this.path ? this.path.split('/').pop()! : this.repo + + // Desktop: materialize the project to a real folder so arduino-cli (via + // tinyService) can read it from disk. The in-memory `mem://` path is not a + // real file path and can't be compiled/uploaded. The browser has no disk to + // write to, so it falls back to the virtual workspace (view/edit only; the + // Arduino service guards mem:// compile/upload with a clear message). + if (fileSystem.isElectron()) { + const dir = `${await window.api.app.getExamplesDir()}/${name}` + for (const [rel, content] of Object.entries(files)) { + await fileSystem.writeFile(`${dir}/${rel}`, content) + } + await new OpenWorkspaceCommand(dir).execute() + return + } + const root = `${VIRTUAL_PREFIX}${this.owner}/${this.repo}${this.path ? '/' + this.path : ''}` virtualFileSystem.clear(root) await virtualFileSystem.seed(root, files) @@ -291,7 +307,6 @@ export class LoadGitHubProjectCommand implements Command { const fsItems = await fileSystem.readDirectory(root, true) const fileItems = buildNestedStructure(fsItems) - const name = this.path ? this.path.split('/').pop()! : this.repo const workspace: Workspace = { id: crypto.randomUUID(), name, diff --git a/src/renderer/src/components/StatusBar.tsx b/src/renderer/src/components/StatusBar.tsx index deb05af..8ff98a1 100644 --- a/src/renderer/src/components/StatusBar.tsx +++ b/src/renderer/src/components/StatusBar.tsx @@ -85,7 +85,7 @@ export function StatusBar(): React.JSX.Element {

UTF-8

Arduino (C++)

-

tinyStudio 0.1.0 · alpha

+

tinyStudio 0.1.1 · alpha

) diff --git a/src/renderer/src/services/arduino/WebSocketArduinoService.ts b/src/renderer/src/services/arduino/WebSocketArduinoService.ts index ea963f9..7161c16 100644 --- a/src/renderer/src/services/arduino/WebSocketArduinoService.ts +++ b/src/renderer/src/services/arduino/WebSocketArduinoService.ts @@ -435,6 +435,12 @@ export class WebSocketArduinoService implements ArduinoService { throw new Error('Arduino client not initialized') } + if (workspacePath.startsWith('mem://')) { + const msg = + 'This example is open in the browser (in-memory) and has no folder on disk for the compiler. Open it in the desktop app, or save it to a local folder, to compile and upload.' + return { success: false, output: msg, errors: [{ message: msg, severity: 'fatal' }] } + } + // Compile the sketch console.log( `Starting compile operation for workspace: ${workspacePath}, FQBN: ${boardConfig.fqbn}` @@ -474,6 +480,12 @@ export class WebSocketArduinoService implements ArduinoService { throw new Error('Arduino client not initialized') } + if ((workspacePathOrBinary || '').startsWith('mem://')) { + const msg = + 'This example is open in the browser (in-memory) and has no folder on disk for the compiler. Open it in the desktop app, or save it to a local folder, to compile and upload.' + return { success: false, output: msg, error: msg } + } + // Upload the sketch console.log( `Starting upload operation to port: ${port}, FQBN: ${boardConfig.fqbn}, workspace: ${workspacePathOrBinary}`