diff --git a/package-lock.json b/package-lock.json
index 5490a33..cc415fa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,11 +12,13 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
+ "avr8js": "^0.21.0",
"better-sqlite3": "^12.9.0",
"commander": "^14.0.3",
"cors": "^2.8.6",
"croner": "^10.0.1",
"dotenv": "^17.4.2",
+ "esptool-js": "^0.4.1",
"express": "^5.2.1",
"imapflow": "^1.3.3",
"mailparser": "^3.9.8",
@@ -26,6 +28,7 @@
"ollama": "^0.6.3",
"playwright": "^1.59.1",
"pptxgenjs": "^4.0.1",
+ "rp2040js": "^1.3.3",
"tesseract.js": "^7.0.0",
"ws": "^8.20.0"
},
@@ -714,6 +717,15 @@
"node": ">=8.0.0"
}
},
+ "node_modules/avr8js": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/avr8js/-/avr8js-0.21.0.tgz",
+ "integrity": "sha512-kjFzxorERow5YFFV25ElhscG57TJqpT6M0m5GDDbXiMYoBmG6K99MQcq6AU1Ly4L+uB1g1o79dMmBNb5O+toxQ==",
+ "engines": {
+ "node": ">= 20.19.0",
+ "npm": ">= 10.0.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1222,6 +1234,44 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
+ "node_modules/esptool-js": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.4.1.tgz",
+ "integrity": "sha512-JhPHyjBncwnZDmOWxzjCQV5e6m1qP9kXPsZy5Zn/cu4tBOfPL8McncIzNVCzmLn7Jvgi06Jg09OK2HrkyMSboA==",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "pako": "^2.1.0",
+ "tslib": "^2.4.1"
+ }
+ },
+ "node_modules/esptool-js/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/esptool-js/node_modules/pako": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -2354,6 +2404,14 @@
"node": ">= 18"
}
},
+ "node_modules/rp2040js": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rp2040js/-/rp2040js-1.3.3.tgz",
+ "integrity": "sha512-6uE6/Ht8L654aF3fB8HS6ICmywzEq1KN71XZq45RR0BUs9SUitPoy+mySoLEHPso6mu2695RCS1hKnchRlGhMA==",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2736,6 +2794,11 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
diff --git a/package.json b/package.json
index 45905a6..acc9bf6 100644
--- a/package.json
+++ b/package.json
@@ -40,11 +40,13 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
+ "avr8js": "^0.21.0",
"better-sqlite3": "^12.9.0",
"commander": "^14.0.3",
"cors": "^2.8.6",
"croner": "^10.0.1",
"dotenv": "^17.4.2",
+ "esptool-js": "^0.4.1",
"express": "^5.2.1",
"imapflow": "^1.3.3",
"mailparser": "^3.9.8",
@@ -54,6 +56,7 @@
"ollama": "^0.6.3",
"playwright": "^1.59.1",
"pptxgenjs": "^4.0.1",
+ "rp2040js": "^1.3.3",
"tesseract.js": "^7.0.0",
"ws": "^8.20.0"
},
diff --git a/src/gateway/routes-arduino.ts b/src/gateway/routes-arduino.ts
index 5203658..f05d14f 100644
--- a/src/gateway/routes-arduino.ts
+++ b/src/gateway/routes-arduino.ts
@@ -521,13 +521,8 @@ app.post('/api/arduino/compile', async (req, res) => {
const fqbnVendor = fqbn.split(':')[0] || '';
const fqbnArch = fqbn.split(':')[1] || '';
- // RP2040 브라우저 에뮬레이터: Serial(USB CDC)을 Serial1(UART0)으로 리다이렉트
- // USB CDC 에뮬레이션은 rp2040js에서 실용적으로 지원되지 않으므로
- // UART0을 통해 시리얼 출력을 받는다.
- const isRP2040Arch = fqbnArch === 'rp2040' || fqbnArch === 'mbed_rp2040';
- const sketchCode = isRP2040Arch
- ? `// [에뮬레이터] Serial → Serial1(UART0) 자동 리다이렉트\n#define Serial Serial1\n\n${code}`
- : code;
+ // RP2040 브라우저 에뮬레이터: USB CDC를 그대로 사용 (Serial → UART0 우회 제거)
+ const sketchCode = code;
fs.writeFileSync(path.join(sketchDir, `${sketchName}.ino`), sketchCode);
// AVR 계열만 .hex 생성 (esp8266은 xtensa → .bin)
const producesHex = fqbnArch === 'avr';
diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts
index 576b445..bb74a44 100644
--- a/src/gateway/server-v2.ts
+++ b/src/gateway/server-v2.ts
@@ -9319,6 +9319,12 @@ app.get('/api/admin/context-viewer', async (req, res) => {
res.json({ sessions: withOnline });
});
+app.use('/vendor', express.static(path.join(webUiPath, 'vendor'), {
+ setHeaders: (res, filePath) => {
+ res.setHeader('Cache-Control', 'no-cache');
+ if (filePath.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
+ }
+}));
app.use(express.static(webUiPath, { setHeaders: (res) => { res.setHeader('Cache-Control', 'no-cache'); } }));
// Serve code directory files for HTML preview (window.open)
diff --git a/web-ui/arduino-emulator.html b/web-ui/arduino-emulator.html
index 1d9b16c..d96a2d8 100644
--- a/web-ui/arduino-emulator.html
+++ b/web-ui/arduino-emulator.html
@@ -310,9 +310,9 @@
/* 속도 슬라이더 */
#speed-range { width:70px; height:4px; accent-color:var(--accent); vertical-align:middle; cursor:pointer; }
-
-
-
+
+
+
@@ -792,7 +792,7 @@ let CPU, AVRIOPort, AVRUSART, AVRTimer, AVRTWI,
timer0Config, timer1Config, timer2Config, twiConfig;
try {
- const mod = await import('https://esm.sh/avr8js@0.21.0');
+ const mod = await import('./vendor/avr8js/index.js');
let avrInstruction;
({ CPU, AVRIOPort, AVRUSART, AVRTimer, AVRTWI,
portAConfig, portBConfig, portCConfig, portDConfig,
@@ -2117,6 +2117,54 @@ let _rp2040Sim = null;
let _rp2040Running = false;
let _rp2040RafId = null;
let _rp2040UartBuf = '';
+let _rp2040PinStates = {};
+let _rp2040UartDec = null;
+let _rp2040BootromU32 = null;
+let _rp2040Uf2Bytes = null;
+let _rp2040Fname = 'sketch.ino';
+let _rp2040BootTimeoutId = null;
+let _origConsoleWarn = null;
+const _RP2040_WARN_FILTER = /Write to invalid SIO address|Read from invalid SIO address|2sio\.js/i;
+const _PICO_LEFT_PINS = [null,null,null,null,null,null,28,null,27,26,null,22,null,21,20,19,18,null,17,16];
+
+function _silenceRp2040jsWarnings() {
+ if (_origConsoleWarn) return;
+ _origConsoleWarn = console.warn;
+ console.warn = function(...args) {
+ const msg = args.join(' ');
+ if (_RP2040_WARN_FILTER.test(msg)) return;
+ return _origConsoleWarn.apply(this, args);
+ };
+}
+
+function _restoreConsoleWarn() {
+ if (_origConsoleWarn) { console.warn = _origConsoleWarn; _origConsoleWarn = null; }
+}
+const _PICO_RIGHT_PINS = [0,1,null,2,3,4,5,null,6,7,8,9,null,10,11,12,13,null,14,15];
+
+function _updateRp2040Board() {
+ const on25 = !!_rp2040PinStates[25];
+ const toolbar = document.getElementById('rp2040-led');
+ if (toolbar) toolbar.style.background = on25 ? '#4ade80' : '#1a2e1a';
+ const svgLed = document.getElementById('pico-led');
+ if (svgLed) svgLed.setAttribute('fill', on25 ? '#22c55e' : '#0a1f0a');
+ _PICO_LEFT_PINS.forEach((g, i) => {
+ if (g === null) return;
+ const el = document.getElementById('rp-lp-' + i);
+ if (!el) return;
+ const st = _rp2040PinStates[g];
+ el.setAttribute('fill', st === undefined ? '#555' : st ? '#4ade80' : '#2a3a2a');
+ el.setAttribute('stroke', st ? '#86efac' : '#aaa');
+ });
+ _PICO_RIGHT_PINS.forEach((g, i) => {
+ if (g === null) return;
+ const el = document.getElementById('rp-rp-' + i);
+ if (!el) return;
+ const st = _rp2040PinStates[g];
+ el.setAttribute('fill', st === undefined ? '#555' : st ? '#4ade80' : '#2a3a2a');
+ el.setAttribute('stroke', st ? '#86efac' : '#aaa');
+ });
+}
let _avrBoardType = 'board_uno';
let _esp8266SessionId = null;
let _esp8266EvtSource = null;
@@ -3348,9 +3396,9 @@ window.stopEmulator = () => {
};
// ── RP2040 emulation ─────────────────────────────────────────────────────────
-const _PICO_BOARD_SVG = `