elemes/proposal.md

1073 lines
34 KiB
Markdown

# 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: `<iframe src="/velxio/editor?embed=true" />`
### 13.4 Board Selection
**Keputusan: Force board dari `VELXIO_CIRCUIT` JSON.** Hide board selector di embed mode. Board di-set otomatis via `elemes:load_circuit`.
### 13.5 Velxio Compile Dependency
**Keputusan: Compile tetap di Velxio backend.** Tidak duplikasi ke Elemes. Siswa klik Run di Velxio → compile + simulate semuanya internal di Velxio.
### 13.6 Apakah Siswa Boleh Menambah Komponen Sendiri?
**Keputusan: Disable component picker di embed mode.** Komponen sudah pre-loaded dari `VELXIO_CIRCUIT`, siswa hanya bisa wiring. Menjaga evaluasi deterministik (fixed component IDs). Mode open sandbox bisa ditambah di Fase 4.
### 13.7 Bagaimana Kalau Velxio Container Down?
**Keputusan: Tampilkan fallback message.** Timeout 10 detik menunggu `velxio:ready`. Jika tidak ada response, tampilkan pesan: "Simulator Arduino sedang tidak tersedia. Hubungi guru jika masalah berlanjut." Lesson C/Python tetap berfungsi normal.
### 13.8 Serial Output: Kapan Mulai Record?
**Keputusan: Reset setiap kali Run.** Evaluasi berdasarkan serial log dari run terakhir. Konsisten dengan behavior Elemes saat ini (output panel di-clear setiap Run).