diff --git a/Dockerfile.standalone b/Dockerfile.standalone index 2c314fa..68c2be0 100644 --- a/Dockerfile.standalone +++ b/Dockerfile.standalone @@ -77,18 +77,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \ | BINDIR=/usr/local/bin sh -# Initialize arduino-cli config, add board manager URLs, then install cores -# - RP2040: earlephilhower fork for Raspberry Pi Pico -# - ESP32: Espressif official (covers ESP32, ESP32-S3, ESP32-C3, etc.) -RUN arduino-cli config init \ - && arduino-cli config add board_manager.additional_urls \ - https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json \ - && arduino-cli config add board_manager.additional_urls \ - https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json \ - && arduino-cli core update-index \ - && arduino-cli core install arduino:avr \ - && arduino-cli core install rp2040:rp2040 \ - && arduino-cli core install esp32:esp32 +# Only install arduino-cli binary here. Core installation (arduino:avr, +# rp2040:rp2040, esp32:esp32) is done at first boot by entrypoint.sh +# and persisted in the mounted /root/.arduino15 volume. +# This keeps the image small (~500MB vs ~2.5GB) and builds fast. WORKDIR /app diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index ed4b270..bf2881c 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -48,6 +48,8 @@ export interface ExampleProject { color: string; }>; thumbnail?: string; + /** Arduino libraries required by this example (auto-installed when loading). */ + libraries?: string[]; } export const exampleProjects: ExampleProject[] = [ @@ -635,6 +637,7 @@ void loop() { id: 'tft-display', title: 'TFT ILI9341 Display', description: 'Color TFT display demo: fills, text, and a bouncing ball animation using the Adafruit ILI9341 library (240x320)', + libraries: ['Adafruit GFX Library', 'Adafruit ILI9341'], category: 'displays', difficulty: 'intermediate', code: `// TFT ILI9341 Display Demo (240x320) @@ -3134,7 +3137,8 @@ void loop() { { id: 'uno-dht22', title: 'Uno: DHT22 Temperature & Humidity', - description: 'Read temperature and humidity using a DHT22 sensor on pin 7. Requires the Adafruit DHT sensor library.', + description: 'Read temperature and humidity using a DHT22 sensor on pin 7.', + libraries: ['DHT sensor library'], category: 'sensors', difficulty: 'beginner', boardFilter: 'arduino-uno', @@ -3427,7 +3431,8 @@ void loop() { { id: 'pico-dht22', title: 'Pico: DHT22 Temperature & Humidity', - description: 'Read temperature and humidity from a DHT22 sensor on GP7 using the Raspberry Pi Pico. Requires Adafruit DHT library.', + description: 'Read temperature and humidity from a DHT22 sensor on GP7 using the Raspberry Pi Pico.', + libraries: ['DHT sensor library'], category: 'sensors', difficulty: 'beginner', boardType: 'raspberry-pi-pico', @@ -3705,7 +3710,8 @@ void loop() { { id: 'esp32-dht22', title: 'ESP32: DHT22 Temperature & Humidity', - description: 'Read temperature and humidity from a DHT22 sensor on GPIO4 of the ESP32. Requires Adafruit DHT library.', + description: 'Read temperature and humidity from a DHT22 sensor on GPIO4 of the ESP32.', + libraries: ['DHT sensor library'], category: 'sensors', difficulty: 'beginner', boardType: 'esp32', @@ -3803,7 +3809,8 @@ void loop() { { id: 'esp32-mpu6050', title: 'ESP32: MPU-6050 Accelerometer', - description: 'Read 3-axis acceleration and gyroscope data from an MPU-6050 over I2C (SDA=D21, SCL=D22). Requires the Adafruit MPU6050 library.', + description: 'Read 3-axis acceleration and gyroscope data from an MPU-6050 over I2C (SDA=D21, SCL=D22).', + libraries: ['Adafruit MPU6050', 'Adafruit Unified Sensor'], category: 'sensors', difficulty: 'intermediate', boardType: 'esp32', @@ -3908,6 +3915,7 @@ void loop() { id: 'esp32-servo', title: 'ESP32: Servo Motor + Potentiometer', description: 'Control a servo motor angle directly with a potentiometer. The servo follows the pot position in real time.', + libraries: ['ESP32Servo'], category: 'robotics', difficulty: 'beginner', boardType: 'esp32', @@ -4004,6 +4012,7 @@ void loop() { id: 'c3-dht22', title: 'ESP32-C3: DHT22 Temperature & Humidity', description: 'Read temperature and humidity with a DHT22 sensor on GPIO3 of the ESP32-C3 RISC-V board.', + libraries: ['DHT sensor library'], category: 'sensors', difficulty: 'beginner', boardType: 'esp32-c3', @@ -4148,6 +4157,7 @@ void loop() { id: 'c3-servo', title: 'ESP32-C3: Servo Motor Sweep', description: 'Sweep a servo motor from 0° to 180° and back on the ESP32-C3 using GPIO10 (PWM).', + libraries: ['ESP32Servo'], category: 'robotics', difficulty: 'beginner', boardType: 'esp32-c3', diff --git a/frontend/src/pages/ExamplesPage.tsx b/frontend/src/pages/ExamplesPage.tsx index 474aaf1..bdace9d 100644 --- a/frontend/src/pages/ExamplesPage.tsx +++ b/frontend/src/pages/ExamplesPage.tsx @@ -4,7 +4,7 @@ * Displays the examples gallery */ -import React from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ExamplesGallery } from '../components/examples/ExamplesGallery'; import { AppHeader } from '../components/layout/AppHeader'; @@ -13,6 +13,7 @@ import { useEditorStore } from '../store/useEditorStore'; import { useSimulatorStore } from '../store/useSimulatorStore'; import { useVfsStore } from '../store/useVfsStore'; import { isBoardComponent } from '../utils/boardPinMapping'; +import { getInstalledLibraries, installLibrary } from '../services/libraryService'; import type { ExampleProject } from '../data/examples'; import type { BoardKind } from '../types/board'; @@ -27,9 +28,36 @@ export const ExamplesPage: React.FC = () => { const navigate = useNavigate(); const { setCode } = useEditorStore(); const { setComponents, setWires, setBoardType, activeBoardId, boards, addBoard, removeBoard, setActiveBoardId } = useSimulatorStore(); + const [installing, setInstalling] = useState<{ total: number; done: number; current: string } | null>(null); - const handleLoadExample = (example: ExampleProject) => { - console.log('Loading example:', example.title); + /** Install any missing libraries required by the example (non-blocking UI). */ + const ensureLibraries = async (libs: string[]): Promise => { + if (libs.length === 0) return; + try { + const installed = await getInstalledLibraries(); + const installedNames = new Set( + installed.map((l) => (l.library?.name ?? l.name ?? '').toLowerCase()) + ); + const missing = libs.filter((l) => !installedNames.has(l.toLowerCase())); + if (missing.length === 0) return; + + setInstalling({ total: missing.length, done: 0, current: missing[0] }); + for (let i = 0; i < missing.length; i++) { + setInstalling({ total: missing.length, done: i, current: missing[i] }); + await installLibrary(missing[i]); + } + setInstalling(null); + } catch { + // If install fails (e.g. offline), continue anyway — compile will show the error + setInstalling(null); + } + }; + + const handleLoadExample = async (example: ExampleProject) => { + // Auto-install required libraries before loading + if (example.libraries && example.libraries.length > 0) { + await ensureLibraries(example.libraries); + } if (example.boards && example.boards.length > 0) { // ── Multi-board loading ─────────────────────────────────────────────── @@ -150,6 +178,36 @@ export const ExamplesPage: React.FC = () => {
+ + {/* Library install overlay */} + {installing && ( +
+
+
+ Installing libraries ({installing.done + 1}/{installing.total}) +
+
+ {installing.current} +
+
+
+
+
+
+ )}
); };