# Proposal: Integrasi Velxio sebagai Simulator Mikrokontroler di Elemes LMS ## Daftar Isi 1. [Latar Belakang](#1-latar-belakang) 2. [Keputusan Arsitektur](#2-keputusan-arsitektur) 3. [Arsitektur Integrasi](#3-arsitektur-integrasi) 4. [Format Lesson Markdown](#4-format-lesson-markdown) 5. [PostMessage Bridge Protocol](#5-postmessage-bridge-protocol) 6. [Sistem Evaluasi](#6-sistem-evaluasi) 7. [Modifikasi Velxio (Fork)](#7-modifikasi-velxio-fork) 8. [Modifikasi Elemes](#8-modifikasi-elemes) 9. [Workflow Guru](#9-workflow-guru) 10. [Deployment](#10-deployment) 11. [Lisensi](#11-lisensi) 12. [Roadmap](#12-roadmap) 13. [Pertanyaan Terbuka](#13-pertanyaan-terbuka) --- ## 1. Latar Belakang ### 1.1 Elemes Saat Ini Elemes adalah LMS untuk mengajar pemrograman dan elektronika. Saat ini mendukung: - **Mode C** — editor + gcc backend + evaluasi stdout - **Mode Python** — editor + python exec + evaluasi stdout - **Mode Circuit** — CircuitJS1 iframe + evaluasi node voltage Setiap mode menggunakan tab system (info | exercise | editor | circuit | output) dan evaluasi berbasis: - `expected_output` — stdout matching - `expected_circuit_output` — node voltage JSON - `key_text` — keyword wajib di source code ### 1.2 Kebutuhan Baru Materi Arduino/mikrokontroler membutuhkan kemampuan: - Menulis dan compile kode Arduino C++ - Simulasi CPU (AVR8) di browser - Komponen visual interaktif (LED, LCD, sensor, dll) - Serial Monitor - Wiring interaktif (siswa merangkai sendiri) ### 1.3 Mengapa Velxio [Velxio](https://github.com/davidmonterocrespo24/velxio) adalah simulator Arduino open-source yang sudah menyediakan semua kebutuhan di atas: - 19 board (Arduino Uno/Mega/Nano, ESP32, Pico, dll) - 48+ komponen visual (wokwi-elements) - CPU emulation nyata (avr8js, rp2040js) - Wire system dengan drag-and-drop - Serial Monitor - Compile via arduino-cli - Self-hosted via Docker Membangun semua ini dari nol membutuhkan ~2-3 minggu. Integrasi Velxio via iframe + postMessage bridge membutuhkan ~3-5 hari. --- ## 2. Keputusan Arsitektur ### 2.1 Velxio Mengambil Alih Workspace (Exclusive Mode) Ketika lesson mengandung `---INITIAL_CODE_ARDUINO---`, Velxio di-embed sebagai iframe dan **menggantikan** seluruh workspace Elemes. Marker `---INITIAL_CODE---` (C), `---INITIAL_CODE_PYTHON---`, dan tab system yang lama **diabaikan** untuk lesson tersebut. **Alasan:** Velxio sudah bundle editor + simulator + serial monitor + wire system. Memecahnya ke tab Elemes akan merusak UX dan mempersulit integrasi. ``` Lesson C/Python (existing): ┌────────┬──────────┬──────────┐ │ Info │ Editor │ Output │ │ │ (Elemes) │ (stdout) │ └────────┴──────────┴──────────┘ Lesson Arduino (baru): ┌────────┬───────────────────────────────┐ │ Info │ Velxio │ │ │ (editor+sim+serial+wiring) │ │Exercise│ │ │ │ [iframe] │ └────────┴───────────────────────────────┘ ``` ### 2.2 Initial Circuit via Markdown JSON Guru mendesain rangkaian di Velxio standalone, export sebagai JSON, lalu paste ke blok `---VELXIO_CIRCUIT---` di lesson markdown. Saat lesson dimuat, Elemes mengirim JSON ini ke Velxio iframe via postMessage. **Alasan:** Velxio menyimpan project ke database (karena pakai akun), tapi Elemes tidak perlu database — cukup JSON di markdown yang di-serve saat lesson load. ### 2.3 Evaluasi: Serial Output + Key Text + Wiring Tiga jenis evaluasi yang disepakati: | Tipe Evaluasi | Objek | Metode | |---------------|-------|--------| | **Program** | Serial output (USART) | String matching: actual vs `expected_serial_output` | | **Program** | Source code | Keyword check: `key_text` ada di source code | | **Circuit** | Wiring topology | Graph comparison: student wires vs `expected_wiring` | **Keputusan penting:** IO state (pin HIGH/LOW) **tidak** dievaluasi secara otomatis. LED nyala, servo berputar, dll adalah **feedback visual** bagi siswa — bukan objek evaluasi. Alasannya: - IO state berubah terhadap waktu (non-deterministic snapshot) - Evaluasi time-based memerlukan recording + timing CPU cycle (effort tinggi) - Timing avr8js ≠ wall clock (simulasi bisa lebih cepat dari real-time) - Guru harus paham timing mikrokontroler untuk menulis expected IO pattern - Serial output sudah cukup membuktikan program berjalan benar untuk konteks edukasi ### 2.4 Component ID Fixed dari Initial Circuit Saat guru menyediakan `VELXIO_CIRCUIT`, component ID sudah ditetapkan (`led1`, `r1`, dll). Siswa tidak perlu drag komponen baru — komponen sudah ada di canvas, siswa hanya perlu wiring. **Alasan:** Evaluasi wiring bergantung pada component ID. Kalau siswa drag komponen sendiri, ID-nya auto-generated dan tidak cocok dengan expected wiring. Dengan ID fixed, evaluasi wiring menjadi deterministik. --- ## 3. Arsitektur Integrasi ### 3.1 Overview ``` ┌─────────────────── Browser ───────────────────────┐ │ │ │ ┌─── Elemes (host page) ────────────────────┐ │ │ │ │ │ │ │ Lesson Info + Exercise │ │ │ │ VelxioBridge.js ◄──── postMessage ──────┐│ │ │ │ Evaluate button ││ │ │ │ ││ │ │ │ ┌─── Velxio (iframe) ─────────────────┐ ││ │ │ │ │ │ ││ │ │ │ │ EmbedBridge.ts ───── postMessage ──┘│ │ │ │ │ │ Monaco Editor (source code) │ │ │ │ │ │ Simulator Canvas (komponen+wiring) │ │ │ │ │ │ Serial Monitor (USART output) │ │ │ │ │ │ Toolbar (Compile/Run/Stop) │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ┌─────────────────── Docker ────────────────────────┐ │ Elemes container (Flask, port 3000) │ │ Velxio container (FastAPI + arduino-cli, port 3080│ └────────────────────────────────────────────────────┘ ``` ### 3.2 Data Flow ``` LESSON LOAD: Elemes parse markdown → extract INITIAL_CODE_ARDUINO → extract VELXIO_CIRCUIT JSON → render iframe src="velxio:3080/editor?embed=true" → on velxio:ready → send elemes:load_code + elemes:load_circuit SISWA BEKERJA: Siswa edit code di Velxio editor Siswa wiring komponen di Velxio canvas Siswa klik Run → Velxio compile + simulate internally Serial output tampil di Velxio Serial Monitor LED nyala, LCD tampil → feedback visual EVALUASI (klik Submit di Elemes): Elemes send elemes:get_source_code → Velxio respond velxio:source_code Elemes send elemes:get_serial_log → Velxio respond velxio:serial_log Elemes send elemes:get_wires → Velxio respond velxio:wires Elemes evaluate: 1. serial_log vs expected_serial_output 2. source_code vs key_text 3. wires vs expected_wiring → pass/fail ``` --- ## 4. Format Lesson Markdown ### 4.1 Lesson Arduino Murni (Code Only) ```markdown ---LESSON_INFO--- **Learning Objectives:** - Memahami fungsi setup() dan loop() - Menggunakan Serial.println() **Prerequisites:** - Dasar pemrograman C ---END_LESSON_INFO--- # Serial Hello World Program pertama Arduino: mencetak teks ke Serial Monitor. ---EXERCISE--- Buat program Arduino yang mencetak "Hello, World!" ke Serial Monitor. --- ---INITIAL_CODE_ARDUINO--- void setup() { // Inisialisasi Serial dengan baud rate 9600 } void loop() { // Cetak "Hello, World!" lalu delay 1 detik } ---END_INITIAL_CODE_ARDUINO--- ---EXPECTED_SERIAL_OUTPUT--- Hello, World! ---END_EXPECTED_SERIAL_OUTPUT--- ---KEY_TEXT--- Serial.begin Serial.println ---END_KEY_TEXT--- ``` ### 4.2 Lesson Arduino + Circuit (Hybrid) ```markdown ---LESSON_INFO--- **Learning Objectives:** - Mengontrol LED dengan digitalWrite - Merangkai LED + resistor ke Arduino **Prerequisites:** - Serial Hello World ---END_LESSON_INFO--- # Blink LED Menyalakan dan mematikan LED di pin 13 setiap 500ms. ---EXERCISE--- ### Bagian 1: Wiring Hubungkan komponen sesuai skema: - Pin 13 → Resistor 220Ω → LED (Anoda) → GND ### Bagian 2: Kode Buat program yang toggle LED dan cetak status ke Serial Monitor. --- ---INITIAL_CODE_ARDUINO--- void setup() { // Inisialisasi pin 13 sebagai OUTPUT // Inisialisasi Serial } void loop() { // Nyalakan LED, cetak "ON", delay 500ms // Matikan LED, cetak "OFF", delay 500ms } ---END_INITIAL_CODE_ARDUINO--- ---VELXIO_CIRCUIT--- { "board": "arduino:avr:uno", "components": [ { "type": "wokwi-led", "id": "led1", "x": 350, "y": 180, "rotation": 0, "props": { "color": "red" } }, { "type": "wokwi-resistor", "id": "r1", "x": 280, "y": 180, "rotation": 0, "props": { "value": "220" } } ], "wires": [] } ---END_VELXIO_CIRCUIT--- ---EXPECTED_SERIAL_OUTPUT--- ON OFF ON OFF ---END_EXPECTED_SERIAL_OUTPUT--- ---EXPECTED_WIRING--- [ ["board:13", "r1:1"], ["r1:2", "led1:A"], ["led1:C", "board:GND"] ] ---END_EXPECTED_WIRING--- ---KEY_TEXT--- digitalWrite delay Serial.println pinMode ---END_KEY_TEXT--- ``` ### 4.3 Lesson Circuit Only (Wiring Tanpa Kode) ```markdown ---LESSON_INFO--- **Learning Objectives:** - Memahami rangkaian seri LED + resistor - Memahami fungsi resistor sebagai pembatas arus ---END_LESSON_INFO--- # Rangkaian Dasar LED Pelajari cara merangkai LED dengan resistor pembatas arus. ---EXERCISE--- Hubungkan rangkaian berikut: - 5V → Resistor 220Ω → LED → GND --- ---VELXIO_CIRCUIT--- { "board": "arduino:avr:uno", "components": [ { "type": "wokwi-led", "id": "led1", "x": 350, "y": 180, "props": { "color": "green" } }, { "type": "wokwi-resistor", "id": "r1", "x": 280, "y": 180, "props": { "value": "220" } } ], "wires": [] } ---END_VELXIO_CIRCUIT--- ---EXPECTED_WIRING--- [ ["board:5V", "r1:1"], ["r1:2", "led1:A"], ["led1:C", "board:GND"] ] ---END_EXPECTED_WIRING--- ``` Catatan: tidak ada `INITIAL_CODE_ARDUINO` → Velxio di-embed dengan `hideEditor=true`, hanya menampilkan canvas simulator. ### 4.4 Deteksi Mode (Backward Compatible) ```python # Prioritas mode detection: # 1. INITIAL_CODE_ARDUINO ada → mode velxio (abaikan INITIAL_CODE, INITIAL_CODE_PYTHON) # 2. INITIAL_CODE_PYTHON ada → mode python (existing) # 3. INITIAL_CODE ada → mode c (existing) # 4. INITIAL_CIRCUIT saja → mode circuit (CircuitJS, existing) # # VELXIO_CIRCUIT hanya di-parse jika mode = velxio # INITIAL_CIRCUIT (CircuitJS format) diabaikan jika mode = velxio ``` ### 4.5 Ringkasan Semua Marker | Marker | Mode | Status | |--------|------|--------| | `---INITIAL_CODE---` | C | Existing | | `---INITIAL_CODE_PYTHON---` | Python | Existing | | `---INITIAL_CIRCUIT---` | Circuit (CircuitJS) | Existing | | `---INITIAL_CODE_ARDUINO---` | **Velxio (baru)** | Baru | | `---VELXIO_CIRCUIT---` | **Velxio (baru)** | Baru | | `---EXPECTED_OUTPUT---` | C/Python | Existing | | `---EXPECTED_SERIAL_OUTPUT---` | **Velxio (baru)** | Baru | | `---EXPECTED_WIRING---` | **Velxio (baru)** | Baru | | `---EXPECTED_CIRCUIT_OUTPUT---` | Circuit (CircuitJS) | Existing | | `---KEY_TEXT---` | Semua mode | Existing | | `---LESSON_INFO---` | Semua mode | Existing | | `---EXERCISE---` | Semua mode | Existing | --- ## 5. PostMessage Bridge Protocol ### 5.1 Elemes → Velxio (Commands) | Message Type | Payload | Kapan Dikirim | |-------------|---------|---------------| | `elemes:load_code` | `{ files: [{ name, content }] }` | Saat lesson load | | `elemes:load_circuit` | `{ board, components, wires }` | Saat lesson load | | `elemes:compile_and_run` | `{}` | Opsional: trigger dari Elemes | | `elemes:stop` | `{}` | Opsional: stop dari Elemes | | `elemes:reset` | `{}` | Opsional: reset simulation | | `elemes:get_source_code` | `{}` | Saat evaluasi (submit) | | `elemes:get_serial_log` | `{}` | Saat evaluasi (submit) | | `elemes:get_wires` | `{}` | Saat evaluasi (submit) | | `elemes:set_embed_mode` | `{ hideEditor?, hideAuth?, hideToolbar? }` | Saat iframe load | ### 5.2 Velxio → Elemes (Events/Responses) | Message Type | Payload | Kapan Dikirim | |-------------|---------|---------------| | `velxio:ready` | `{ version }` | Saat Velxio iframe selesai load | | `velxio:source_code` | `{ files: [{ name, content }] }` | Response ke `get_source_code` | | `velxio:serial_log` | `{ log: string }` | Response ke `get_serial_log` | | `velxio:wires` | `{ wires: [{ start, end, signalType }] }` | Response ke `get_wires` | | `velxio:compile_result` | `{ success, errors? }` | Setelah compile selesai | | `velxio:sim_start` | `{}` | Saat simulation mulai | | `velxio:sim_stop` | `{}` | Saat simulation berhenti | ### 5.3 Sequence Diagram: Load Lesson ``` Elemes Velxio (iframe) │ │ │ render iframe │ │ src="velxio/editor?embed=true" │ │ ────────────────────────────────>│ │ │ │ velxio:ready │ │ <────────────────────────────────│ │ │ │ elemes:set_embed_mode │ │ {hideAuth:true} │ │ ────────────────────────────────>│ │ │ │ elemes:load_circuit │ │ {board, components, wires:[]} │ │ ────────────────────────────────>│ → inject ke useSimulatorStore │ │ │ elemes:load_code │ │ {files:[{name:"sketch.ino", │ │ content:"void setup()..."}]} │ │ ────────────────────────────────>│ → inject ke useEditorStore │ │ │ (siswa bekerja...) │ ``` ### 5.4 Sequence Diagram: Evaluasi ``` Elemes Velxio (iframe) │ │ │ [Siswa klik Submit] │ │ │ │ elemes:get_source_code │ │ ────────────────────────────────>│ │ velxio:source_code │ │ <────────────────────────────────│ │ │ │ elemes:get_serial_log │ │ ────────────────────────────────>│ │ velxio:serial_log │ │ <────────────────────────────────│ │ │ │ elemes:get_wires │ │ ────────────────────────────────>│ │ velxio:wires │ │ <────────────────────────────────│ │ │ │ [Elemes evaluasi lokal] │ │ ✓ serial_log vs expected │ │ ✓ source_code vs key_text │ │ ✓ wires vs expected_wiring │ │ → PASS / FAIL │ ``` --- ## 6. Sistem Evaluasi ### 6.1 Serial Output Matching Membandingkan akumulasi output USART selama simulasi berjalan dengan `expected_serial_output`. ```python # Pseudocode evaluasi def evaluate_serial(actual_log: str, expected: str) -> bool: actual_lines = actual_log.strip().splitlines() expected_lines = expected.strip().splitlines() # Cek: expected lines harus muncul di actual (in order) # Actual boleh lebih panjang (program terus loop) j = 0 for line in actual_lines: if j < len(expected_lines) and line.strip() == expected_lines[j].strip(): j += 1 if j == len(expected_lines): return True return j == len(expected_lines) ``` **Catatan:** Evaluasi subsequence, bukan exact match — karena program Arduino biasanya loop terus, serial output bisa lebih panjang dari expected. ### 6.2 Key Text Check Sama dengan sistem yang sudah ada di Elemes — cek apakah keyword wajib ada di source code. ```python def evaluate_key_text(source_code: str, keywords: list[str]) -> bool: return all(kw in source_code for kw in keywords) ``` ### 6.3 Wiring Topology Check Membandingkan koneksi wire siswa vs expected wiring sebagai **undirected graph edges**. ```javascript function evaluateWiring(studentWires, expectedConnections) { // Normalize setiap wire menjadi sorted pair string const normalize = (a, b) => [a, b].sort().join('↔'); const studentEdges = new Set( studentWires.map(w => normalize( `${w.start.componentId}:${w.start.pinName}`, `${w.end.componentId}:${w.end.pinName}` )) ); const expectedEdges = expectedConnections.map( ([a, b]) => normalize(a, b) ); // Semua expected edge harus ada di student wiring const allPresent = expectedEdges.every(e => studentEdges.has(e)); // Opsional: tidak boleh ada wire berlebih (strict mode) // const noExtra = studentEdges.size === expectedEdges.length; return allPresent; } ``` **Keputusan:** Evaluasi hanya cek expected edges **ada** di student wiring. Wire berlebih (misalnya siswa tambah wire extra) **tidak** menyebabkan fail. Ini lebih forgiving untuk konteks edukasi. ### 6.4 Kombinasi Evaluasi Per Tipe Lesson | Tipe Lesson | Serial | Key Text | Wiring | Lulus jika | |-------------|--------|----------|--------|-----------| | Code only | ✓ | ✓ | — | Serial + Key Text pass | | Circuit only | — | — | ✓ | Wiring pass | | Hybrid | ✓ | ✓ | ✓ | Semua pass | --- ## 7. Modifikasi Velxio (Fork) ### 7.1 File Baru | File | Deskripsi | LOC estimasi | |------|-----------|-------------| | `frontend/src/services/EmbedBridge.ts` | PostMessage bridge terpusat: kirim event, terima command, dispatch ke store | ~150 | ### 7.2 File yang Dimodifikasi | # | File | Modifikasi | LOC estimasi | |---|------|-----------|-------------| | 1 | `frontend/src/simulation/AVRSimulator.ts` | Akumulasi serial output ke buffer, expose `getSerialLog()` | ~15 | | 2 | `frontend/src/store/useSimulatorStore.ts` | (a) Tambah action `loadCircuit(data)` untuk inject components+wires dari JSON. (b) Tambah action `getWires()` return current wires. (c) Init EmbedBridge saat store create. | ~40 | | 3 | `frontend/src/store/useEditorStore.ts` | Tambah action `loadFiles(files)` untuk inject code dari parent (sudah ada `loadFiles` — tinggal expose ke bridge) | ~10 | | 4 | `frontend/src/pages/EditorPage.tsx` | Parse query param `?embed=true`, hide AppHeader, hide auth UI, hide save/login modal, opsional hide editor/sidebar | ~30 | | 5 | `frontend/src/components/editor/EditorToolbar.tsx` | Tambah tombol "Export Circuit JSON" (copy to clipboard). Hanya tampil di non-embed mode (untuk workflow guru). | ~15 | | 6 | `frontend/vite.config.ts` | Opsional: konfigurasi CSP header untuk iframe embedding | ~5 | **Total modifikasi: ~265 LOC** ### 7.3 Detail: EmbedBridge.ts ```typescript // frontend/src/services/EmbedBridge.ts import { useSimulatorStore } from '../store/useSimulatorStore'; import { useEditorStore } from '../store/useEditorStore'; class EmbedBridge { private isEmbedded: boolean; constructor() { this.isEmbedded = window.parent !== window; if (this.isEmbedded) { window.addEventListener('message', this.onMessage.bind(this)); } } // Dipanggil setelah Velxio fully loaded notifyReady() { this.send('velxio:ready', { version: '1.0' }); } private send(type: string, payload: any = {}) { if (!this.isEmbedded) return; window.parent.postMessage({ type, ...payload }, '*'); } private onMessage(event: MessageEvent) { const { type } = event.data || {}; if (!type?.startsWith('elemes:')) return; switch (type) { case 'elemes:load_code': useEditorStore.getState().loadFiles( event.data.files.map((f: any) => ({ id: f.name, name: f.name, content: f.content, modified: false })) ); break; case 'elemes:load_circuit': useSimulatorStore.getState().loadCircuit(event.data); break; case 'elemes:get_source_code': const files = useEditorStore.getState().files; this.send('velxio:source_code', { files: files.map(f => ({ name: f.name, content: f.content })) }); break; case 'elemes:get_serial_log': const log = useSimulatorStore.getState().getSerialLog(); this.send('velxio:serial_log', { log }); break; case 'elemes:get_wires': const wires = useSimulatorStore.getState().wires; this.send('velxio:wires', { wires }); break; case 'elemes:set_embed_mode': // Dispatch ke EditorPage via global state atau event window.dispatchEvent( new CustomEvent('velxio-embed-mode', { detail: event.data }) ); break; case 'elemes:compile_and_run': // Trigger compile + run programmatically break; case 'elemes:stop': useSimulatorStore.getState().stopSimulation(); break; } } } export const embedBridge = new EmbedBridge(); ``` ### 7.4 Detail: Export Circuit JSON (Workflow Guru) ```typescript // Tambah di EditorToolbar.tsx function exportCircuitJSON() { const { components, wires } = useSimulatorStore.getState(); const board = /* current selected board FQBN */; const circuitData = { board, components: components.map(c => ({ type: c.type, id: c.id, x: c.x, y: c.y, rotation: c.rotation || 0, props: c.props || {} })), wires: wires.map(w => ({ start: { componentId: w.start.componentId, pinName: w.start.pinName }, end: { componentId: w.end.componentId, pinName: w.end.pinName }, signalType: w.signalType })) }; const json = JSON.stringify(circuitData, null, 2); navigator.clipboard.writeText(json); alert('Circuit JSON copied to clipboard!'); } ``` Guru juga bisa export expected wiring: ```typescript function exportExpectedWiring() { const { wires } = useSimulatorStore.getState(); const wiringArray = wires.map(w => [ `${w.start.componentId}:${w.start.pinName}`, `${w.end.componentId}:${w.end.pinName}` ]); const json = JSON.stringify(wiringArray, null, 2); navigator.clipboard.writeText(json); alert('Expected wiring JSON copied to clipboard!'); } ``` --- ## 8. Modifikasi Elemes ### 8.1 File Baru | File | Deskripsi | |------|-----------| | `frontend/src/lib/services/velxio-bridge.js` | PostMessage bridge sisi Elemes: kirim command, terima response, evaluasi | ### 8.2 File yang Dimodifikasi | # | File | Modifikasi | |---|------|-----------| | 1 | `services/lesson_service.py` | Parse marker baru: `INITIAL_CODE_ARDUINO`, `VELXIO_CIRCUIT`, `EXPECTED_SERIAL_OUTPUT`, `EXPECTED_WIRING` | | 2 | `routes/lessons.py` | Serve field Arduino/Velxio ke frontend JSON response | | 3 | `frontend/src/routes/lesson/[slug]/+page.svelte` | Conditional render: jika mode=velxio → render iframe + VelxioBridge, else → existing UI | | 4 | `podman-compose.yml` | Tambah Velxio service | ### 8.3 Detail: VelxioBridge.js ```javascript // frontend/src/lib/services/velxio-bridge.js export class VelxioBridge { constructor(iframe) { this.iframe = iframe; this.pending = {}; // pending request-response this.serialLog = ''; this._onReady = null; window.addEventListener('message', this._onMessage.bind(this)); } // === Lifecycle === onReady(callback) { this._onReady = callback; } destroy() { window.removeEventListener('message', this._onMessage.bind(this)); } // === Commands === loadCode(files) { this._send('elemes:load_code', { files }); } loadCircuit(circuitData) { this._send('elemes:load_circuit', circuitData); } setEmbedMode(options) { this._send('elemes:set_embed_mode', options); } // === Evaluation === async evaluate(expected) { const results = {}; // 1. Source code → key text if (expected.key_text) { const { files } = await this._request('elemes:get_source_code', 'velxio:source_code'); const allCode = files.map(f => f.content).join('\n'); results.key_text = expected.key_text.every(kw => allCode.includes(kw)); } // 2. Serial output if (expected.serial_output) { const { log } = await this._request('elemes:get_serial_log', 'velxio:serial_log'); results.serial = this._matchSerial(log, expected.serial_output); } // 3. Wiring if (expected.wiring) { const { wires } = await this._request('elemes:get_wires', 'velxio:wires'); results.wiring = this._matchWiring(wires, expected.wiring); } results.pass = Object.values(results) .filter(v => typeof v === 'boolean') .every(Boolean); return results; } // === Internal === _send(type, payload = {}) { this.iframe.contentWindow.postMessage({ type, ...payload }, '*'); } _request(sendType, expectType) { return new Promise((resolve) => { this.pending[expectType] = resolve; this._send(sendType); // Timeout safety setTimeout(() => { if (this.pending[expectType]) { delete this.pending[expectType]; resolve(null); } }, 3000); }); } _onMessage(event) { const { type } = event.data || {}; if (!type?.startsWith('velxio:')) return; if (type === 'velxio:ready' && this._onReady) { this._onReady(); } if (this.pending[type]) { this.pending[type](event.data); delete this.pending[type]; } } _matchSerial(actual, expected) { const actualLines = actual.trim().split('\n').map(l => l.trim()); const expectedLines = expected.trim().split('\n').map(l => l.trim()); let j = 0; for (const line of actualLines) { if (j < expectedLines.length && line === expectedLines[j]) j++; if (j === expectedLines.length) return true; } return j === expectedLines.length; } _matchWiring(studentWires, expectedPairs) { const norm = (a, b) => [a, b].sort().join('↔'); const studentEdges = new Set( studentWires.map(w => norm( `${w.start.componentId}:${w.start.pinName}`, `${w.end.componentId}:${w.end.pinName}` ) ) ); return expectedPairs.every(([a, b]) => studentEdges.has(norm(a, b))); } } ``` --- ## 9. Workflow Guru ### 9.1 Membuat Lesson Arduino + Circuit ``` 1. Buka Velxio standalone (http://localhost:3080) 2. Pilih board (Arduino Uno) 3. Tambah komponen dari Component Picker - Drag LED ke canvas - Drag Resistor ke canvas - Atur posisi 4. Wiring rangkaian (sebagai contoh jawaban) - Pin 13 → Resistor → LED → GND 5. Klik "Export Circuit JSON" (tombol baru di fork) → JSON component+wires ter-copy ke clipboard → Ini jadi isi blok ---VELXIO_CIRCUIT--- 6. Klik "Export Expected Wiring" (tombol baru di fork) → JSON edges ter-copy ke clipboard → Ini jadi isi blok ---EXPECTED_WIRING--- 7. Hapus semua wires (biarkan komponen saja) 8. Klik "Export Circuit JSON" lagi → JSON tanpa wires ter-copy ke clipboard → Ini jadi isi blok ---VELXIO_CIRCUIT--- yang sebenarnya (komponen ada, wires kosong — siswa yang harus wiring) 9. Buka file lesson.md - Paste VELXIO_CIRCUIT (tanpa wires) - Paste EXPECTED_WIRING (dari langkah 6) - Tulis INITIAL_CODE_ARDUINO - Tulis EXPECTED_SERIAL_OUTPUT - Tulis KEY_TEXT 10. Jalankan ./elemes.sh generatetoken → update CSV 11. Test lesson sebagai siswa ``` ### 9.2 Membuat Lesson Code Only (Tanpa Wiring) ``` 1. Buka file lesson.md 2. Tulis INITIAL_CODE_ARDUINO (tanpa VELXIO_CIRCUIT) 3. Tulis EXPECTED_SERIAL_OUTPUT 4. Tulis KEY_TEXT 5. Selesai — Velxio akan embed tanpa circuit canvas ``` --- ## 10. Deployment ### 10.1 Docker Compose ```yaml # podman-compose.yml version: '3.8' services: elemes: build: ./elemes ports: - "3000:3000" volumes: - ../content:/app/content:ro - ../assets:/app/assets:ro - ../tokens_siswa.csv:/app/tokens.csv environment: - VELXIO_URL=http://localhost:3080 velxio: image: ghcr.io/a2nr/velxio-elemes:latest # atau: build dari fork # build: ./velxio-fork ports: - "3080:80" environment: - SECRET_KEY=embed-only-no-auth-needed ``` ### 10.2 Nginx/Caddy Reverse Proxy (Opsional) Jika mau serve kedua service di satu domain: ``` https://lms.sekolah.id/ → Elemes https://lms.sekolah.id/velxio/ → Velxio (iframe src) ``` ### 10.3 Image Size | Container | Estimasi Size | |-----------|---------------| | Elemes | ~200MB (Python + gcc) | | Velxio | ~1.5GB (Node + Python + arduino-cli + avr core) | | Total | ~1.7GB | Velxio image besar karena arduino-cli + platform cores. Ini satu kali download, compile berjalan lokal. --- ## 11. Lisensi ### 11.1 Velxio: AGPLv3 Velxio menggunakan dual licensing: - AGPLv3 untuk personal/educational/open-source - Commercial license untuk proprietary/SaaS **Implikasi untuk Elemes:** Elemes di-deploy secara self-hosted (Podman + Tailscale) untuk lingkungan sekolah, bukan sebagai SaaS publik. Dalam konteks ini: - AGPLv3 mengharuskan: jika Velxio dimodifikasi dan di-deploy sebagai network service, source code modifikasi harus tersedia. - Solusi: **Fork publik** di Gitea/GitHub. Semua modifikasi EmbedBridge sudah open source. ### 11.2 Library Inti: MIT | Library | Lisensi | Dipakai oleh | |---------|---------|-------------| | avr8js | MIT | Velxio (internal) | | wokwi-elements | MIT | Velxio (internal) | | rp2040js | MIT | Velxio (internal) | Tidak ada masalah lisensi tambahan dari library inti. --- ## 12. Roadmap ### Fase 1: Velxio Fork + Embed Mode (3 hari) - [ ] Fork Velxio repository - [ ] Implementasi `EmbedBridge.ts` - [ ] Modifikasi `EditorPage.tsx` — embed mode (hide auth/header) - [ ] Modifikasi `useSimulatorStore.ts` — `loadCircuit()` action - [ ] Akumulasi serial log di `AVRSimulator.ts` - [ ] Tombol "Export Circuit JSON" + "Export Expected Wiring" di toolbar - [ ] Build Docker image fork - [ ] Test: load circuit via postMessage, ambil serial log, ambil wires ### Fase 2: Elemes Integration (2 hari) - [ ] `VelxioBridge.js` — bridge sisi Elemes - [ ] `lesson_service.py` — parse marker baru - [ ] `routes/lessons.py` — serve field Arduino - [ ] `lesson/[slug]/+page.svelte` — conditional render iframe - [ ] Evaluasi: serial + key_text + wiring - [ ] Update `podman-compose.yml` - [ ] Test end-to-end: lesson load → siswa wiring + coding → submit → evaluate ### Fase 3: Content + Polish (2 hari) - [ ] Buat 3 contoh lesson Arduino (Hello Serial, Blink LED, Button Input) - [ ] Buat 2 contoh lesson circuit-only (LED circuit, Voltage divider) - [ ] Update `examples/` folder - [ ] Update `elemes.sh init` untuk include contoh Arduino - [ ] Update README guru - [ ] Test sebagai "guru" dan "siswa" ### Fase 4: Enhancement (Opsional, Nanti) - [ ] Velxio embed: preselect board dari URL param - [ ] Velxio embed: lock komponen (siswa tidak bisa drag/delete) - [ ] Velxio embed: disable component picker (siswa tidak bisa tambah komponen baru) - [ ] Lesson review mode: tampilkan solution wiring overlay - [ ] Multiple board support per lesson - [ ] Library management dari Elemes (install Arduino library) --- ## 13. Keputusan (Resolved) ### 13.1 Pin Naming Convention **Keputusan:** Ditentukan setelah Velxio fork berjalan. Buat rangkaian test, wiring manual, inspect `wire.start.pinName` / `wire.end.pinName` di console, lalu tulis konvensi berdasarkan observasi. ### 13.2 Strict vs Lenient Wiring Evaluation **Keputusan: Lenient.** Expected wires harus ada di student wiring, wire berlebih OK. Tanpa `FORBIDDEN_WIRING` untuk tahap awal — bisa ditambah di Fase 4 jika dibutuhkan. ### 13.3 Embed URL: Same Origin via Tailscale Serve **Keputusan: Same-origin.** Velxio di-proxy lewat Tailscale Serve di path `/velxio/`, sehingga satu domain dengan Elemes. Tidak perlu origin validation atau konfigurasi CSP. Tambah handler di `config/sinau-c-tail.json`: ```json "/velxio/": { "Proxy": "http://velxio:3080/" } ``` Iframe src di Elemes: `