Merge pull request #90 from davidmonterocrespo24/feature/vscode-extension

Feature/vscode extension
pull/74/merge
David Montero Crespo 2026-04-04 18:04:22 -03:00 committed by GitHub
commit 9aab48ec64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 3880 additions and 0 deletions

499
vscode-extension/package-lock.json generated Normal file
View File

@ -0,0 +1,499 @@
{
"name": "velxio-simulator",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "velxio-simulator",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/vscode": "^1.85.0",
"esbuild": "^0.20.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.85.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
"license": "ISC"
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.110.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz",
"integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@ -0,0 +1,140 @@
{
"name": "velxio-simulator",
"displayName": "Velxio Simulator",
"description": "Local Arduino & ESP32 simulator for VS Code — compile, simulate, and debug embedded projects without leaving your editor",
"version": "0.1.0",
"publisher": "velxio",
"license": "MIT",
"icon": "media/icon.png",
"repository": {
"type": "git",
"url": "https://github.com/davidmonterocrespo24/velxio"
},
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Other",
"Testing",
"Education"
],
"keywords": [
"arduino",
"esp32",
"simulator",
"emulator",
"embedded",
"micropython",
"raspberry-pi-pico",
"wokwi",
"electronics"
],
"activationEvents": [
"workspaceContains:velxio.toml",
"workspaceContains:diagram.json"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "velxio.openSimulator",
"title": "Open Simulator",
"category": "Velxio",
"icon": "$(circuit-board)"
},
{
"command": "velxio.compile",
"title": "Compile Sketch",
"category": "Velxio",
"icon": "$(tools)"
},
{
"command": "velxio.run",
"title": "Run Simulation",
"category": "Velxio",
"icon": "$(play)"
},
{
"command": "velxio.stop",
"title": "Stop Simulation",
"category": "Velxio",
"icon": "$(debug-stop)"
},
{
"command": "velxio.selectBoard",
"title": "Select Board",
"category": "Velxio",
"icon": "$(list-selection)"
}
],
"menus": {
"editor/title": [
{
"command": "velxio.run",
"when": "resourceExtname == .ino || resourceExtname == .py",
"group": "navigation"
}
]
},
"configuration": {
"title": "Velxio Simulator",
"properties": {
"velxio.defaultBoard": {
"type": "string",
"default": "arduino-uno",
"enum": [
"arduino-uno",
"arduino-nano",
"arduino-mega",
"raspberry-pi-pico",
"pi-pico-w",
"esp32",
"esp32-s3",
"esp32-c3",
"attiny85"
],
"description": "Default board type for new projects"
},
"velxio.autoStartBackend": {
"type": "boolean",
"default": true,
"description": "Automatically start the compilation backend when needed"
},
"velxio.backendPort": {
"type": "number",
"default": 0,
"description": "Fixed port for the backend (0 = auto-assign)"
},
"velxio.arduinoCliPath": {
"type": "string",
"default": "",
"description": "Path to arduino-cli executable (leave empty to auto-detect)"
}
}
},
"jsonValidation": [
{
"fileMatch": "diagram.json",
"url": "./schemas/diagram.schema.json"
}
]
},
"scripts": {
"vscode:prepublish": "npm run build",
"build": "npm run build:extension && npm run build:webview",
"build:extension": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node18 --sourcemap",
"build:webview": "cd webview && npm run build",
"watch": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node18 --sourcemap --watch",
"lint": "eslint src/",
"package": "vsce package"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/vscode": "^1.85.0",
"esbuild": "^0.20.0",
"typescript": "^5.3.0"
},
"dependencies": {
"@iarna/toml": "^2.2.5"
}
}

View File

@ -0,0 +1,59 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Velxio Diagram",
"description": "Circuit diagram layout (Wokwi-compatible format)",
"type": "object",
"required": ["version", "parts", "connections"],
"properties": {
"version": {
"type": "integer",
"const": 1
},
"author": { "type": "string" },
"editor": { "type": "string" },
"parts": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "left", "top"],
"properties": {
"id": { "type": "string" },
"type": { "type": "string" },
"left": { "type": "number" },
"top": { "type": "number" },
"rotate": { "type": "number" },
"hide": { "type": "boolean" },
"attrs": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
}
},
"connections": {
"type": "array",
"items": {
"type": "array",
"minItems": 3,
"maxItems": 4,
"items": [
{ "type": "string", "description": "Source pin (componentId:pinName)" },
{ "type": "string", "description": "Target pin (componentId:pinName)" },
{ "type": "string", "description": "Wire color" },
{
"type": "array",
"items": { "type": "string" },
"description": "Routing hints"
}
]
}
},
"serialMonitor": {
"type": "object",
"properties": {
"display": { "type": "string", "enum": ["terminal", "plotter"] },
"newline": { "type": "string", "enum": ["lf", "cr", "crlf"] }
}
}
}
}

View File

@ -0,0 +1,170 @@
/**
* BackendManager Spawns and manages the Velxio FastAPI backend process.
*
* The backend provides:
* - /api/compile Arduino sketch compilation via arduino-cli
* - /api/simulation/ws WebSocket bridge to QEMU for ESP32 simulation
*
* AVR/RP2040 boards don't need the backend (simulation runs in the WebView).
*/
import * as vscode from 'vscode';
import { ChildProcess, spawn } from 'child_process';
import * as net from 'net';
import * as path from 'path';
import * as fs from 'fs';
export class BackendManager {
private process: ChildProcess | null = null;
private _port = 0;
private _ready = false;
private outputChannel: vscode.OutputChannel;
constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
}
get port(): number { return this._port; }
get ready(): boolean { return this._ready; }
get apiBase(): string { return `http://localhost:${this._port}/api`; }
/** Start the backend on a free port. Returns the port number. */
async start(): Promise<number> {
if (this.process && this._ready) return this._port;
this._port = await this.findFreePort();
const configPort = vscode.workspace.getConfiguration('velxio').get<number>('backendPort');
if (configPort && configPort > 0) {
this._port = configPort;
}
// Find the backend directory
const backendDir = await this.findBackendDir();
if (!backendDir) {
throw new Error('Velxio backend not found. Please install the Velxio backend or set the path in settings.');
}
this.outputChannel.appendLine(`[Backend] Starting on port ${this._port}...`);
this.outputChannel.appendLine(`[Backend] Directory: ${backendDir}`);
// Spawn uvicorn
const python = this.findPython(backendDir);
this.process = spawn(python, [
'-m', 'uvicorn',
'app.main:app',
'--port', String(this._port),
'--host', '127.0.0.1',
], {
cwd: backendDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, PYTHONUNBUFFERED: '1' },
});
this.process.stdout?.on('data', (data) => {
this.outputChannel.appendLine(`[Backend] ${data.toString().trim()}`);
});
this.process.stderr?.on('data', (data) => {
this.outputChannel.appendLine(`[Backend] ${data.toString().trim()}`);
});
this.process.on('exit', (code) => {
this.outputChannel.appendLine(`[Backend] Process exited (code=${code})`);
this._ready = false;
this.process = null;
});
// Wait for the server to be ready
await this.waitForReady();
this._ready = true;
this.outputChannel.appendLine(`[Backend] Ready at ${this.apiBase}`);
return this._port;
}
/** Stop the backend process */
async stop(): Promise<void> {
if (!this.process) return;
this.outputChannel.appendLine('[Backend] Stopping...');
this.process.kill('SIGTERM');
// Give it 3 seconds to shut down gracefully
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
this.process?.kill('SIGKILL');
resolve();
}, 3000);
this.process?.on('exit', () => {
clearTimeout(timeout);
resolve();
});
});
this.process = null;
this._ready = false;
}
/** Find the backend directory (relative to extension or workspace) */
private async findBackendDir(): Promise<string | null> {
// 1. Check relative to the extension (monorepo layout)
const extensionDir = path.resolve(__dirname, '..');
const monorepoBackend = path.resolve(extensionDir, '..', 'backend');
if (fs.existsSync(path.join(monorepoBackend, 'app', 'main.py'))) {
return monorepoBackend;
}
// 2. Check workspace folders
for (const folder of vscode.workspace.workspaceFolders ?? []) {
const wsBackend = path.join(folder.uri.fsPath, 'backend');
if (fs.existsSync(path.join(wsBackend, 'app', 'main.py'))) {
return wsBackend;
}
}
return null;
}
/** Find the correct Python executable (venv or system) */
private findPython(backendDir: string): string {
// Check for venv
const venvPaths = [
path.join(backendDir, 'venv', 'Scripts', 'python.exe'), // Windows
path.join(backendDir, 'venv', 'bin', 'python'), // Unix
path.join(backendDir, '.venv', 'Scripts', 'python.exe'),
path.join(backendDir, '.venv', 'bin', 'python'),
];
for (const p of venvPaths) {
if (fs.existsSync(p)) return p;
}
return 'python';
}
/** Find a free TCP port */
private findFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const srv = net.createServer();
srv.listen(0, '127.0.0.1', () => {
const addr = srv.address() as net.AddressInfo;
srv.close(() => resolve(addr.port));
});
srv.on('error', reject);
});
}
/** Wait for the backend health endpoint to respond */
private async waitForReady(maxRetries = 30): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(`http://127.0.0.1:${this._port}/docs`);
if (response.ok) return;
} catch {
// Not ready yet
}
await new Promise(r => setTimeout(r, 1000));
}
throw new Error(`Backend failed to start after ${maxRetries} seconds`);
}
}

View File

@ -0,0 +1,50 @@
/**
* FileWatcher Watches sketch files for changes and triggers recompilation.
*/
import * as vscode from 'vscode';
export class FileWatcher {
private watcher: vscode.FileSystemWatcher | null = null;
private debounceTimer: NodeJS.Timeout | null = null;
private _onChange = new vscode.EventEmitter<vscode.Uri>();
/** Fired when a sketch file changes (debounced by 500ms) */
public readonly onChange = this._onChange.event;
/** Start watching sketch files */
start(): void {
if (this.watcher) return;
this.watcher = vscode.workspace.createFileSystemWatcher(
'**/*.{ino,h,cpp,c,py}',
false, // creations
false, // changes
false, // deletions
);
const debouncedFire = (uri: vscode.Uri) => {
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => this._onChange.fire(uri), 500);
};
this.watcher.onDidChange(debouncedFire);
this.watcher.onDidCreate(debouncedFire);
this.watcher.onDidDelete(debouncedFire);
}
/** Stop watching */
stop(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.watcher?.dispose();
this.watcher = null;
}
dispose(): void {
this.stop();
this._onChange.dispose();
}
}

View File

@ -0,0 +1,91 @@
/**
* ProjectConfig Reads velxio.toml and diagram.json from the workspace.
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import TOML from '@iarna/toml';
import type { VelxioConfig, DiagramJson, BoardKind } from './types';
export class ProjectConfig {
private workspaceRoot: string;
constructor(workspaceRoot: string) {
this.workspaceRoot = workspaceRoot;
}
/** Read and parse velxio.toml from the workspace root */
readVelxioToml(): VelxioConfig | null {
const tomlPath = path.join(this.workspaceRoot, 'velxio.toml');
if (!fs.existsSync(tomlPath)) return null;
try {
const content = fs.readFileSync(tomlPath, 'utf-8');
const parsed = TOML.parse(content) as unknown as VelxioConfig;
return parsed;
} catch (err) {
vscode.window.showWarningMessage(`Failed to parse velxio.toml: ${err}`);
return null;
}
}
/** Read and parse diagram.json from the workspace root */
readDiagramJson(): DiagramJson | null {
const jsonPath = path.join(this.workspaceRoot, 'diagram.json');
if (!fs.existsSync(jsonPath)) return null;
try {
const content = fs.readFileSync(jsonPath, 'utf-8');
return JSON.parse(content) as DiagramJson;
} catch (err) {
vscode.window.showWarningMessage(`Failed to parse diagram.json: ${err}`);
return null;
}
}
/** Resolve the board kind from config or settings */
getBoard(): BoardKind {
const config = this.readVelxioToml();
if (config?.velxio?.board) {
return config.velxio.board as BoardKind;
}
return vscode.workspace.getConfiguration('velxio').get<BoardKind>('defaultBoard') ?? 'arduino-uno';
}
/** Get the language mode (arduino or micropython) */
getLanguageMode(): 'arduino' | 'micropython' {
const config = this.readVelxioToml();
return config?.velxio?.language ?? 'arduino';
}
/** Get the pre-compiled firmware path (if specified) */
getFirmwarePath(): string | null {
const config = this.readVelxioToml();
if (!config?.velxio?.firmware) return null;
return path.resolve(this.workspaceRoot, config.velxio.firmware);
}
/** Collect all sketch files (.ino, .h, .cpp, .py) from the workspace */
async getSketchFiles(): Promise<Array<{ name: string; content: string }>> {
const language = this.getLanguageMode();
const pattern = language === 'micropython' ? '**/*.py' : '**/*.{ino,h,cpp,c}';
const uris = await vscode.workspace.findFiles(pattern, '**/node_modules/**');
const files: Array<{ name: string; content: string }> = [];
for (const uri of uris) {
const relativePath = path.relative(this.workspaceRoot, uri.fsPath);
const content = fs.readFileSync(uri.fsPath, 'utf-8');
files.push({ name: relativePath, content });
}
return files;
}
/** Create a default velxio.toml in the workspace */
async createDefaultConfig(board: BoardKind): Promise<void> {
const tomlPath = path.join(this.workspaceRoot, 'velxio.toml');
const content = `[velxio]\nversion = 1\nboard = "${board}"\n`;
fs.writeFileSync(tomlPath, content, 'utf-8');
}
}

View File

@ -0,0 +1,77 @@
/**
* SerialTerminal VS Code pseudo-terminal for simulation serial I/O.
*
* Provides a native VS Code terminal that displays serial output from the
* running simulation and sends user input back to the simulated UART.
*/
import * as vscode from 'vscode';
export class SerialTerminal {
private terminal: vscode.Terminal | null = null;
private writeEmitter = new vscode.EventEmitter<string>();
private closeEmitter = new vscode.EventEmitter<number | void>();
private _onInput = new vscode.EventEmitter<string>();
/** Fired when the user types in the terminal */
public readonly onInput = this._onInput.event;
/** Create and show the serial terminal */
open(boardName: string): void {
if (this.terminal) {
this.terminal.show();
return;
}
const pty: vscode.Pseudoterminal = {
onDidWrite: this.writeEmitter.event,
onDidClose: this.closeEmitter.event,
open: () => {
this.writeEmitter.fire(`\x1b[36m--- Velxio Serial Monitor (${boardName}) ---\x1b[0m\r\n`);
},
close: () => {
this.terminal = null;
},
handleInput: (data: string) => {
this._onInput.fire(data);
// Echo input in a dim color
this.writeEmitter.fire(`\x1b[90m${data}\x1b[0m`);
},
};
this.terminal = vscode.window.createTerminal({
name: `Velxio Serial (${boardName})`,
pty,
iconPath: new vscode.ThemeIcon('circuit-board'),
});
this.terminal.show(true); // preserve focus
}
/** Write serial output to the terminal */
write(text: string): void {
if (!this.terminal) return;
// Convert \n to \r\n for terminal display
this.writeEmitter.fire(text.replace(/\n/g, '\r\n'));
}
/** Clear the terminal */
clear(): void {
this.writeEmitter.fire('\x1b[2J\x1b[H'); // ANSI clear + home
}
/** Close the terminal */
close(): void {
if (this.terminal) {
this.closeEmitter.fire();
this.terminal.dispose();
this.terminal = null;
}
}
dispose(): void {
this.close();
this.writeEmitter.dispose();
this.closeEmitter.dispose();
this._onInput.dispose();
}
}

View File

@ -0,0 +1,198 @@
/**
* SimulatorPanel Manages the VS Code WebView panel that hosts the simulation UI.
*
* The WebView runs a stripped-down version of the Velxio React frontend
* (SimulatorCanvas + SerialMonitor) with simulation engines (avr8js, rp2040js)
* running directly in the WebView's JavaScript context.
*
* Communication with the extension host uses VS Code's postMessage API.
*/
import * as vscode from 'vscode';
import * as path from 'path';
import type { ToWebviewMessage, FromWebviewMessage, BoardKind } from './types';
export class SimulatorPanel {
public static readonly viewType = 'velxio.simulator';
private static instance: SimulatorPanel | undefined;
private readonly panel: vscode.WebviewPanel;
private readonly extensionUri: vscode.Uri;
private disposables: vscode.Disposable[] = [];
private _ready = false;
private _onSerialOutput = new vscode.EventEmitter<string>();
private _onSimulationState = new vscode.EventEmitter<boolean>();
private _onReady = new vscode.EventEmitter<void>();
/** Fired when serial data arrives from the simulation */
public readonly onSerialOutput = this._onSerialOutput.event;
/** Fired when simulation starts/stops */
public readonly onSimulationState = this._onSimulationState.event;
/** Fired when the WebView is ready */
public readonly onReady = this._onReady.event;
public get ready(): boolean { return this._ready; }
/** Get or create the singleton panel */
public static createOrShow(extensionUri: vscode.Uri): SimulatorPanel {
const column = vscode.ViewColumn.Beside;
if (SimulatorPanel.instance) {
SimulatorPanel.instance.panel.reveal(column);
return SimulatorPanel.instance;
}
const panel = vscode.window.createWebviewPanel(
SimulatorPanel.viewType,
'Velxio Simulator',
column,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(extensionUri, 'dist', 'webview'),
vscode.Uri.joinPath(extensionUri, 'media'),
],
},
);
SimulatorPanel.instance = new SimulatorPanel(panel, extensionUri);
return SimulatorPanel.instance;
}
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this.panel = panel;
this.extensionUri = extensionUri;
this.panel.webview.html = this.getHtmlContent();
// Handle messages from the WebView
this.panel.webview.onDidReceiveMessage(
(msg: FromWebviewMessage) => this.handleMessage(msg),
null,
this.disposables,
);
// Cleanup on dispose
this.panel.onDidDispose(() => this.dispose(), null, this.disposables);
}
/** Send a message to the WebView */
public postMessage(message: ToWebviewMessage): void {
if (this._ready) {
this.panel.webview.postMessage(message);
}
}
/** Send compiled hex to the WebView */
public loadHex(hex: string, board: BoardKind): void {
this.postMessage({ type: 'loadHex', hex, board });
}
/** Send firmware binary (base64) for ESP32 */
public loadBinary(firmwareBase64: string, board: BoardKind): void {
this.postMessage({ type: 'loadBinary', firmwareBase64, board });
}
/** Load MicroPython files */
public loadMicroPython(files: Array<{ name: string; content: string }>, board: BoardKind): void {
this.postMessage({ type: 'loadMicroPython', files, board });
}
/** Start the simulation */
public start(): void {
this.postMessage({ type: 'start' });
}
/** Stop the simulation */
public stop(): void {
this.postMessage({ type: 'stop' });
}
/** Send serial input text */
public serialInput(text: string): void {
this.postMessage({ type: 'serialInput', text });
}
/** Set the board type */
public setBoard(board: BoardKind): void {
this.postMessage({ type: 'setBoard', board });
}
/** Set the backend API base URL */
public setApiBase(apiBase: string): void {
this.postMessage({ type: 'setApiBase', apiBase });
}
private handleMessage(msg: FromWebviewMessage): void {
switch (msg.type) {
case 'ready':
this._ready = true;
this._onReady.fire();
break;
case 'serialOutput':
this._onSerialOutput.fire(msg.text);
break;
case 'simulationState':
this._onSimulationState.fire(msg.running);
break;
case 'error':
vscode.window.showErrorMessage(`Velxio: ${msg.message}`);
break;
case 'log':
// Forward WebView logs to the output channel
break;
}
}
private getHtmlContent(): string {
const webview = this.panel.webview;
const webviewDistUri = vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview');
// In development, point to local files; in production, use bundled assets
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(webviewDistUri, 'index.js'));
const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(webviewDistUri, 'index.css'));
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}' 'unsafe-eval';
img-src ${webview.cspSource} data: https:;
font-src ${webview.cspSource};
connect-src http://127.0.0.1:* ws://127.0.0.1:* https://micropython.org;
">
<link rel="stylesheet" href="${styleUri}">
<title>Velxio Simulator</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
private dispose(): void {
SimulatorPanel.instance = undefined;
this._onSerialOutput.dispose();
this._onSimulationState.dispose();
this._onReady.dispose();
for (const d of this.disposables) d.dispose();
this.disposables = [];
}
}
function getNonce(): string {
let text = '';
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += chars.charAt(Math.floor(Math.random() * chars.length));
}
return text;
}

View File

@ -0,0 +1,285 @@
/**
* Velxio VS Code Extension Entry point
*
* Provides commands to compile, simulate, and interact with Arduino/ESP32
* sketches directly within VS Code. Simulation runs locally using avr8js,
* rp2040js (in the WebView), and QEMU (via the backend) for ESP32 boards.
*/
import * as vscode from 'vscode';
import { SimulatorPanel } from './SimulatorPanel';
import { BackendManager } from './BackendManager';
import { ProjectConfig } from './ProjectConfig';
import { SerialTerminal } from './SerialTerminal';
import { FileWatcher } from './FileWatcher';
import { BOARD_LABELS, type BoardKind } from './types';
let backend: BackendManager;
let serialTerminal: SerialTerminal;
let fileWatcher: FileWatcher;
let outputChannel: vscode.OutputChannel;
let statusBarItem: vscode.StatusBarItem;
export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('Velxio');
backend = new BackendManager(outputChannel);
serialTerminal = new SerialTerminal();
fileWatcher = new FileWatcher();
// Status bar item showing current board
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
statusBarItem.command = 'velxio.selectBoard';
statusBarItem.tooltip = 'Click to change board';
updateStatusBar('arduino-uno');
// ── Commands ──────────────────────────────────────────────────────────────
context.subscriptions.push(
vscode.commands.registerCommand('velxio.openSimulator', () => {
const panel = SimulatorPanel.createOrShow(context.extensionUri);
setupPanelListeners(panel, context);
statusBarItem.show();
}),
vscode.commands.registerCommand('velxio.compile', async () => {
await compileAndLoad(context);
}),
vscode.commands.registerCommand('velxio.run', async () => {
const panel = SimulatorPanel.createOrShow(context.extensionUri);
setupPanelListeners(panel, context);
if (!panel.ready) {
// Wait for the WebView to initialize
await new Promise<void>(resolve => {
const disposable = panel.onReady(() => { disposable.dispose(); resolve(); });
});
}
await compileAndLoad(context);
panel.start();
}),
vscode.commands.registerCommand('velxio.stop', () => {
const panel = SimulatorPanel.createOrShow(context.extensionUri);
panel.stop();
}),
vscode.commands.registerCommand('velxio.selectBoard', async () => {
const boards = Object.entries(BOARD_LABELS) as [BoardKind, string][];
const items = boards.map(([kind, label]) => ({
label,
description: kind,
kind: kind,
}));
const selected = await vscode.window.showQuickPick(items, {
placeHolder: 'Select a board',
title: 'Velxio: Select Board',
});
if (selected) {
const boardKind = selected.description as BoardKind;
updateStatusBar(boardKind);
// Update velxio.toml if it exists
const workspaceRoot = getWorkspaceRoot();
if (workspaceRoot) {
const config = new ProjectConfig(workspaceRoot);
const existingConfig = config.readVelxioToml();
if (existingConfig) {
await config.createDefaultConfig(boardKind);
}
}
// Update the WebView
try {
const panel = SimulatorPanel.createOrShow(context.extensionUri);
panel.setBoard(boardKind);
} catch {
// Panel not open yet, that's fine
}
}
}),
);
// ── Auto-activation ───────────────────────────────────────────────────────
// If velxio.toml or diagram.json exists, show the status bar
const workspaceRoot = getWorkspaceRoot();
if (workspaceRoot) {
const config = new ProjectConfig(workspaceRoot);
const velxioConfig = config.readVelxioToml();
if (velxioConfig) {
updateStatusBar(config.getBoard());
statusBarItem.show();
}
}
// ── Cleanup ───────────────────────────────────────────────────────────────
context.subscriptions.push(
outputChannel,
statusBarItem,
serialTerminal,
fileWatcher,
{ dispose: () => { backend.stop(); } },
);
outputChannel.appendLine('Velxio extension activated');
}
export function deactivate() {
backend.stop();
fileWatcher.stop();
}
// ── Helpers ─────────────────────────────────────────────────────────────────
function getWorkspaceRoot(): string | null {
const folders = vscode.workspace.workspaceFolders;
return folders?.[0]?.uri.fsPath ?? null;
}
function updateStatusBar(board: BoardKind): void {
const label = BOARD_LABELS[board] ?? board;
statusBarItem.text = `$(circuit-board) ${label}`;
}
let panelListenersSet = false;
function setupPanelListeners(panel: SimulatorPanel, context: vscode.ExtensionContext): void {
if (panelListenersSet) return;
panelListenersSet = true;
// Wire serial output to the VS Code terminal
panel.onSerialOutput((text) => {
serialTerminal.write(text);
});
// Wire terminal input back to the simulation
serialTerminal.onInput((text) => {
panel.serialInput(text);
});
// When the panel is ready, send initial configuration
panel.onReady(async () => {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) return;
const config = new ProjectConfig(workspaceRoot);
const board = config.getBoard();
panel.setBoard(board);
// Read diagram.json if it exists
const diagram = config.readDiagramJson();
if (diagram) {
panel.postMessage({ type: 'setDiagram', diagram });
}
// Start backend if needed (for ESP32 boards)
if (needsBackend(board)) {
try {
await backend.start();
panel.setApiBase(backend.apiBase);
} catch (err) {
outputChannel.appendLine(`[Backend] Failed to start: ${err}`);
}
}
});
}
function needsBackend(board: BoardKind): boolean {
// Arduino compilation always needs the backend
// ESP32 boards also need QEMU via the backend WebSocket
return true; // For MVP, always start the backend
}
async function compileAndLoad(context: vscode.ExtensionContext): Promise<void> {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
vscode.window.showErrorMessage('No workspace folder open');
return;
}
const config = new ProjectConfig(workspaceRoot);
const board = config.getBoard();
const language = config.getLanguageMode();
const panel = SimulatorPanel.createOrShow(context.extensionUri);
setupPanelListeners(panel, context);
// Check for pre-compiled firmware first
const firmwarePath = config.getFirmwarePath();
if (firmwarePath) {
outputChannel.appendLine(`[Compile] Loading pre-compiled firmware: ${firmwarePath}`);
const fs = await import('fs');
const data = fs.readFileSync(firmwarePath);
if (firmwarePath.endsWith('.hex')) {
panel.loadHex(data.toString('utf-8'), board);
} else {
panel.loadBinary(data.toString('base64'), board);
}
return;
}
// MicroPython: just send .py files
if (language === 'micropython') {
const files = await config.getSketchFiles();
panel.loadMicroPython(files, board);
return;
}
// Arduino: compile via backend
try {
await backend.start();
const files = await config.getSketchFiles();
outputChannel.appendLine(`[Compile] Compiling ${files.length} files for ${board}...`);
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: 'Velxio: Compiling...' },
async () => {
const response = await fetch(`${backend.apiBase}/compile`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
files: files.map(f => ({ name: f.name, content: f.content })),
board_fqbn: getBoardFqbn(board),
}),
});
if (!response.ok) {
const error = await response.json() as { detail?: string };
throw new Error(error.detail ?? `Compilation failed (${response.status})`);
}
const result = await response.json() as { hex?: string; binary?: string };
if (result.hex) {
panel.loadHex(result.hex, board);
outputChannel.appendLine('[Compile] Success — hex loaded');
}
},
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(`Compilation failed: ${msg}`);
outputChannel.appendLine(`[Compile] Error: ${msg}`);
}
}
function getBoardFqbn(board: BoardKind): string {
const fqbnMap: Record<string, string> = {
'arduino-uno': 'arduino:avr:uno',
'arduino-nano': 'arduino:avr:nano:cpu=atmega328',
'arduino-mega': 'arduino:avr:mega',
'raspberry-pi-pico': 'rp2040:rp2040:rpipico',
'pi-pico-w': 'rp2040:rp2040:rpipicow',
'esp32': 'esp32:esp32:esp32',
'esp32-s3': 'esp32:esp32:esp32s3',
'esp32-c3': 'esp32:esp32:esp32c3',
'attiny85': 'ATTinyCore:avr:attinyx5:chip=85,clock=internal16mhz',
};
return fqbnMap[board] ?? 'arduino:avr:uno';
}

View File

@ -0,0 +1,83 @@
/**
* Shared types for extension WebView communication.
*/
export type BoardKind =
| 'arduino-uno'
| 'arduino-nano'
| 'arduino-mega'
| 'raspberry-pi-pico'
| 'pi-pico-w'
| 'esp32'
| 'esp32-s3'
| 'esp32-c3'
| 'attiny85';
export type LanguageMode = 'arduino' | 'micropython';
/** Messages from the VS Code extension → WebView */
export type ToWebviewMessage =
| { type: 'loadHex'; hex: string; board: BoardKind }
| { type: 'loadBinary'; firmwareBase64: string; board: BoardKind }
| { type: 'loadMicroPython'; files: Array<{ name: string; content: string }>; board: BoardKind }
| { type: 'start' }
| { type: 'stop' }
| { type: 'serialInput'; text: string }
| { type: 'setBoard'; board: BoardKind }
| { type: 'setDiagram'; diagram: DiagramJson }
| { type: 'setApiBase'; apiBase: string };
/** Messages from the WebView → VS Code extension */
export type FromWebviewMessage =
| { type: 'ready' }
| { type: 'serialOutput'; text: string }
| { type: 'simulationState'; running: boolean }
| { type: 'error'; message: string }
| { type: 'requestCompile'; files: Array<{ name: string; content: string }>; board: string; fqbn: string }
| { type: 'log'; level: 'info' | 'warn' | 'error'; message: string };
/** diagram.json format (Wokwi-compatible) */
export interface DiagramJson {
version: 1;
author?: string;
editor?: string;
parts: DiagramPart[];
connections: DiagramConnection[];
serialMonitor?: { display?: string; newline?: string };
}
export interface DiagramPart {
id: string;
type: string;
left: number;
top: number;
rotate?: number;
hide?: boolean;
attrs?: Record<string, string>;
}
/** [fromPin, toPin, wireColor, routingHints?] */
export type DiagramConnection = [string, string, string, string[]?];
/** velxio.toml parsed config */
export interface VelxioConfig {
velxio: {
version: number;
board?: string;
firmware?: string;
elf?: string;
language?: LanguageMode;
};
}
export const BOARD_LABELS: Record<BoardKind, string> = {
'arduino-uno': 'Arduino Uno',
'arduino-nano': 'Arduino Nano',
'arduino-mega': 'Arduino Mega 2560',
'raspberry-pi-pico': 'Raspberry Pi Pico',
'pi-pico-w': 'Raspberry Pi Pico W',
'esp32': 'ESP32 DevKit V1',
'esp32-s3': 'ESP32-S3 DevKit',
'esp32-c3': 'ESP32-C3 DevKit',
'attiny85': 'ATtiny85',
};

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "webview"]
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Velxio Simulator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/webviewApp.tsx"></script>
</body>
</html>

1820
vscode-extension/webview/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
{
"name": "velxio-webview",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "^5.3.0",
"vite": "^6.0.0"
}
}

View File

@ -0,0 +1,61 @@
/**
* WebView Extension bridge
*
* Receives messages from the VS Code extension host and translates them
* into simulation actions. Sends simulation events back to the extension.
*/
// @ts-expect-error — acquireVsCodeApi is injected by VS Code's webview runtime
const vscode = acquireVsCodeApi();
export type ToWebviewMessage =
| { type: 'loadHex'; hex: string; board: string }
| { type: 'loadBinary'; firmwareBase64: string; board: string }
| { type: 'loadMicroPython'; files: Array<{ name: string; content: string }>; board: string }
| { type: 'start' }
| { type: 'stop' }
| { type: 'serialInput'; text: string }
| { type: 'setBoard'; board: string }
| { type: 'setDiagram'; diagram: unknown }
| { type: 'setApiBase'; apiBase: string };
type MessageHandler = (msg: ToWebviewMessage) => void;
const handlers: MessageHandler[] = [];
/** Register a handler for messages from the extension */
export function onMessage(handler: MessageHandler): void {
handlers.push(handler);
}
/** Send a message to the extension host */
export function postToExtension(message: unknown): void {
vscode.postMessage(message);
}
/** Notify the extension that the WebView is ready */
export function notifyReady(): void {
postToExtension({ type: 'ready' });
}
/** Send serial output to the extension */
export function sendSerialOutput(text: string): void {
postToExtension({ type: 'serialOutput', text });
}
/** Send simulation state change to the extension */
export function sendSimulationState(running: boolean): void {
postToExtension({ type: 'simulationState', running });
}
/** Send an error to the extension */
export function sendError(message: string): void {
postToExtension({ type: 'error', message });
}
// Listen for messages from the extension host
window.addEventListener('message', (event) => {
const msg = event.data as ToWebviewMessage;
for (const handler of handlers) {
handler(msg);
}
});

View File

@ -0,0 +1,259 @@
/**
* WebView App Stripped-down Velxio simulator for VS Code WebView.
*
* This is a minimal React app that embeds:
* - SimulatorCanvas (board visualization + components)
* - SerialMonitor (serial output/input)
*
* It does NOT include: routing, auth, Monaco editor, project persistence.
* Communication with the VS Code extension is via postMessage bridge.
*/
import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { onMessage, notifyReady, sendSerialOutput, sendSimulationState, sendError } from './bridge';
/**
* Placeholder app Phase 1 MVP
*
* In the full implementation, this will import and render:
* - SimulatorCanvas from the shared frontend/src/components/
* - SerialMonitor from the shared frontend/src/components/
* - useSimulatorStore from the shared frontend/src/store/
*
* For now, this is a functional skeleton that handles the postMessage
* protocol and shows a basic simulation status panel.
*/
const App: React.FC = () => {
const [board, setBoard] = useState('arduino-uno');
const [running, setRunning] = useState(false);
const [serialOutput, setSerialOutput] = useState('');
const [status, setStatus] = useState('Ready');
const [hexLoaded, setHexLoaded] = useState(false);
useEffect(() => {
onMessage((msg) => {
switch (msg.type) {
case 'setBoard':
setBoard(msg.board);
setStatus(`Board: ${msg.board}`);
break;
case 'loadHex':
setHexLoaded(true);
setStatus(`Firmware loaded for ${msg.board}`);
break;
case 'loadBinary':
setHexLoaded(true);
setStatus(`Binary firmware loaded for ${msg.board}`);
break;
case 'loadMicroPython':
setHexLoaded(true);
setStatus(`MicroPython loaded (${msg.files.length} files)`);
break;
case 'start':
setRunning(true);
setStatus('Simulation running');
sendSimulationState(true);
break;
case 'stop':
setRunning(false);
setStatus('Simulation stopped');
sendSimulationState(false);
break;
case 'serialInput':
setSerialOutput(prev => prev + `> ${msg.text}\n`);
break;
case 'setApiBase':
setStatus(`Backend: ${msg.apiBase}`);
break;
}
});
// Tell the extension we're ready
notifyReady();
}, []);
return (
<div style={styles.container}>
{/* Header */}
<div style={styles.header}>
<span style={styles.logo}>Velxio</span>
<span style={styles.boardBadge}>{board}</span>
<span style={{
...styles.statusDot,
background: running ? '#4caf50' : hexLoaded ? '#ff9800' : '#666',
}} />
<span style={styles.status}>{status}</span>
</div>
{/* Simulation Canvas Placeholder */}
<div style={styles.canvas}>
<div style={styles.canvasPlaceholder}>
<div style={styles.boardIcon}>
{board.includes('esp32') ? '⬡' : board.includes('pico') ? '◆' : '⬤'}
</div>
<div style={styles.boardName}>{board}</div>
{running && (
<div style={styles.runningIndicator}>
<span style={styles.pulsingDot} />
Simulating...
</div>
)}
{!running && !hexLoaded && (
<div style={styles.hint}>
Use <code>Velxio: Run Simulation</code> to start
</div>
)}
{!running && hexLoaded && (
<div style={styles.hint}>
Firmware loaded. Press <strong>Run</strong> to start.
</div>
)}
</div>
</div>
{/* Serial Monitor */}
<div style={styles.serialContainer}>
<div style={styles.serialHeader}>
<span style={styles.serialTitle}>Serial Monitor</span>
{running && <span style={styles.serialBadge}>CONNECTED</span>}
</div>
<pre style={styles.serialOutput}>
{serialOutput || (running ? 'Waiting for serial data...\n' : 'Start simulation to see output.\n')}
</pre>
</div>
</div>
);
};
const styles: Record<string, React.CSSProperties> = {
container: {
display: 'flex',
flexDirection: 'column',
height: '100vh',
background: 'var(--vscode-editor-background, #1e1e1e)',
color: 'var(--vscode-editor-foreground, #cccccc)',
fontFamily: 'var(--vscode-font-family, monospace)',
fontSize: 13,
overflow: 'hidden',
},
header: {
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '6px 12px',
background: 'var(--vscode-titleBar-activeBackground, #3c3c3c)',
borderBottom: '1px solid var(--vscode-panel-border, #333)',
flexShrink: 0,
},
logo: {
fontWeight: 700,
fontSize: 14,
color: 'var(--vscode-textLink-foreground, #4fc3f7)',
},
boardBadge: {
background: 'var(--vscode-badge-background, #0e639c)',
color: 'var(--vscode-badge-foreground, #fff)',
padding: '1px 8px',
borderRadius: 10,
fontSize: 11,
fontWeight: 600,
},
statusDot: {
width: 8,
height: 8,
borderRadius: '50%',
flexShrink: 0,
},
status: {
fontSize: 11,
color: 'var(--vscode-descriptionForeground, #999)',
},
canvas: {
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 0,
background: 'var(--vscode-editor-background, #1e1e1e)',
},
canvasPlaceholder: {
textAlign: 'center' as const,
padding: 40,
},
boardIcon: {
fontSize: 64,
marginBottom: 16,
opacity: 0.5,
},
boardName: {
fontSize: 18,
fontWeight: 600,
marginBottom: 8,
},
runningIndicator: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
color: '#4caf50',
fontSize: 14,
marginTop: 12,
},
pulsingDot: {
width: 10,
height: 10,
borderRadius: '50%',
background: '#4caf50',
animation: 'pulse 1.5s ease-in-out infinite',
},
hint: {
color: 'var(--vscode-descriptionForeground, #888)',
fontSize: 12,
marginTop: 12,
},
serialContainer: {
height: 200,
display: 'flex',
flexDirection: 'column',
borderTop: '1px solid var(--vscode-panel-border, #333)',
flexShrink: 0,
},
serialHeader: {
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '4px 12px',
background: 'var(--vscode-sideBar-background, #252526)',
borderBottom: '1px solid var(--vscode-panel-border, #333)',
},
serialTitle: {
fontWeight: 600,
fontSize: 12,
},
serialBadge: {
background: '#4caf50',
color: '#fff',
padding: '0 6px',
borderRadius: 3,
fontSize: 9,
fontWeight: 700,
},
serialOutput: {
flex: 1,
margin: 0,
padding: 8,
color: '#00ff41',
background: '#0a0a0a',
overflowY: 'auto' as const,
whiteSpace: 'pre-wrap' as const,
fontSize: 12,
lineHeight: 1.4,
},
};
// Mount
const root = document.getElementById('root');
if (root) {
createRoot(root).render(<App />);
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src"]
}

View File

@ -0,0 +1,23 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
build: {
outDir: path.resolve(__dirname, '..', 'dist', 'webview'),
emptyOutDir: true,
rollupOptions: {
input: path.resolve(__dirname, 'src', 'webviewApp.tsx'),
output: {
entryFileNames: 'index.js',
assetFileNames: 'index.[ext]',
// Single chunk for simplicity in WebView loading
manualChunks: undefined,
},
},
// Inline small assets to avoid CSP issues
assetsInlineLimit: 100000,
sourcemap: true,
},
});