34 KiB
Proposal: Integrasi Velxio sebagai Simulator Mikrokontroler di Elemes LMS
Daftar Isi
- Latar Belakang
- Keputusan Arsitektur
- Arsitektur Integrasi
- Format Lesson Markdown
- PostMessage Bridge Protocol
- Sistem Evaluasi
- Modifikasi Velxio (Fork)
- Modifikasi Elemes
- Workflow Guru
- Deployment
- Lisensi
- Roadmap
- 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 matchingexpected_circuit_output— node voltage JSONkey_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 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)
---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)
---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)
---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)
# 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.
# 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.
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.
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
// 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)
// 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:
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
// 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
# 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 Elemeslesson_service.py— parse marker baruroutes/lessons.py— serve field Arduinolesson/[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 inituntuk 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:
"/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).