v3.0.5: RP2040 브라우저 에뮬레이터 — rp2040js 기반 전체 구현

- RP2040(Pico) 보드 감지·컴파일·에뮬레이션 지원 추가
  - Serial → Serial1(UART0) 자동 리다이렉트 (#define 삽입)
  - .uf2 파일 우선 추출, bootrom 자동 다운로드·캐싱
  - ELF .text 추출: arm-none-eabi-objcopy 대신 Node.js 내장 파서
  - HTTP 302 리다이렉트 자동 팔로우 (_httpsGetFollow)
- rp2040js 에뮬레이터: fast-path 초기화 루프, 피코 SVG 보드, GPIO25 LED
- 예외 처리: HardFault(#3)만 중지, 나머지 IRQ/SysTick 정상 통과
- UART onByte: TextDecoder stream 모드로 한글 UTF-8 정상 출력
- heartbeat 메시지 제거
- WiFi scan mock: WSCN 프로토콜로 C++ ↔ 브라우저 동기화, UI 편집 지원
- 에뮬레이터 팝업 → 코드 에디터 인라인 iframe 패널로 전환
- CDN 의존성 → /vendor/ 로컬 서빙 (xterm, Monaco)
- SSE 세션 idle timer (창 닫힘 후 5분 자동 종료)
- 업로드: 속도 자동 재시도(921600→460800→230400) + FlashMode fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-19 13:37:56 +09:00
co-authored by Claude Sonnet 4.6
parent 5a7c38b26d
commit 23831f79ba
7 changed files with 819 additions and 86 deletions
+422 -9
View File
@@ -325,6 +325,7 @@
<button onclick="compileAndRun()" id="btn-compile" title="컴파일 &amp; 실행 (Ctrl+Enter)">▶ 컴파일</button>
<button onclick="togglePower()" id="btn-stop" title="정지 / 시작" disabled>▶ 시작</button>
<button onclick="resetEmulator()" id="btn-reset" title="에뮬레이션 재시작 (리셋)" disabled>↺ 재시작</button>
<span id="rp2040-led" title="GPIO25 (내장 LED)" style="display:none;width:12px;height:12px;border-radius:50%;background:#1a2e1a;border:1px solid #2d4a2d;margin:0 2px;flex-shrink:0"></span>
<button onclick="toggleBB()" id="btn-bb" title="브레드보드 &amp; SPICE 시뮬레이터">🔌 회로</button>
<div class="theme-picker" title="테마">
<button class="tdot active" id="td-light" onclick="setTheme('light')" style="--c:#e5e7eb;border:1px solid #94a3b8" title="라이트"></button>
@@ -571,6 +572,15 @@
<button class="wifi-btn" id="wifi-connect-btn" onclick="_wifiConnectClicked()">연결</button>
<button class="wifi-btn" onclick="_wifiDisconnectClicked()">끊기</button>
</div>
<!-- Scan network mock config -->
<div style="margin-top:8px;border-top:1px solid var(--bdr-in);padding-top:8px">
<div style="font-size:10px;color:var(--muted);font-weight:600;letter-spacing:.06em;margin-bottom:5px">SCAN 목록 (scanNetworks 모의)</div>
<div id="wifi-scan-list" style="display:flex;flex-direction:column;gap:3px;margin-bottom:5px"></div>
<div style="display:flex;gap:4px;margin-bottom:4px">
<button class="wifi-btn" onclick="_wifiScanAddNet()" style="flex:none;padding:2px 8px;font-size:10px">+ 추가</button>
<button class="wifi-btn" onclick="_wifiScanApply()" style="flex:none;padding:2px 8px;font-size:10px;background:var(--accent);border-color:var(--accent);color:#fff">적용</button>
</div>
</div>
</div>
</div>
@@ -617,6 +627,7 @@
</div>
<span style="margin-left:auto;display:flex;gap:4px;align-items:center">
<button class="sch-file-btn" id="sch-gen-btn" onclick="schGenFromCode()" title="스케치 코드를 분석해서 회로도 자동 생성">⚡ 코드→회로</button>
<span id="sch-log" style="font-size:11px;color:#e8c860;white-space:nowrap"></span>
<button class="sch-file-btn" id="sch-gen-code-btn" onclick="schGenCodeFromCircuit()" title="회로도를 분석해서 Arduino 스케치 자동 생성">🤖 회로→코드</button>
<button class="sch-file-btn" onclick="schSaveProject()" title="circuit.json을 프로젝트 폴더에 저장">💾 저장</button>
<button class="sch-file-btn" onclick="schLoadLocal()" title="저장된 회로도 불러오기">📂 불러오기</button>
@@ -1592,6 +1603,7 @@ function _wifiShow() {
const sec = document.getElementById('wifi-section');
if (sec) sec.style.display = '';
_wifiUpdateUI();
_wifiScanRender();
}
function _wifiHandleEvent(msg) {
@@ -1648,6 +1660,47 @@ function _wifiDisconnectClicked() {
}
}
// WiFi scan mock management
let _wifiScanNets = [
{ ssid: 'HomeNetwork', rssi: -45, channel: 1 },
{ ssid: 'Office_5G', rssi: -55, channel: 6 },
{ ssid: 'Neighbor_WiFi', rssi: -65, channel: 11 }
];
function _wifiScanRender() {
const list = document.getElementById('wifi-scan-list');
if (!list) return;
list.innerHTML = '';
_wifiScanNets.forEach((net, idx) => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:3px;font-size:10px';
row.innerHTML =
`<input value="${net.ssid}" placeholder="SSID" style="flex:2;min-width:0;background:var(--panel);border:1px solid var(--bdr2);color:var(--text);border-radius:3px;padding:2px 4px;font-size:10px;font-family:inherit" oninput="_wifiScanNets[${idx}].ssid=this.value">` +
`<input type="number" value="${net.rssi}" min="-100" max="-1" style="width:42px;background:var(--panel);border:1px solid var(--bdr2);color:var(--text);border-radius:3px;padding:2px 4px;font-size:10px;font-family:inherit" oninput="_wifiScanNets[${idx}].rssi=+this.value" title="RSSI (dBm)">` +
`<input type="number" value="${net.channel}" min="1" max="14" style="width:30px;background:var(--panel);border:1px solid var(--bdr2);color:var(--text);border-radius:3px;padding:2px 4px;font-size:10px;font-family:inherit" oninput="_wifiScanNets[${idx}].channel=+this.value" title="Ch">` +
`<button onclick="_wifiScanRemove(${idx})" style="flex:none;padding:1px 5px;border:1px solid var(--bdr2);background:var(--panel);color:var(--muted);border-radius:3px;cursor:pointer;font-size:10px">✕</button>`;
list.appendChild(row);
});
}
function _wifiScanAddNet() {
_wifiScanNets.push({ ssid: 'NewNetwork', rssi: -70, channel: 1 });
_wifiScanRender();
}
function _wifiScanRemove(idx) {
_wifiScanNets.splice(idx, 1);
_wifiScanRender();
}
function _wifiScanApply() {
if (!_IS_ESP32 || !_esp32SessionId) return;
fetch('/api/arduino/qemu/' + _esp32SessionId + '/scan', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ networks: _wifiScanNets })
}).catch(() => {});
}
function _showSection(id) {
const el = document.getElementById(id);
if (el) el.style.display = '';
@@ -2059,6 +2112,12 @@ function _esp32PinState(gpio) {
// ── ESP8266 state ─────────────────────────────────────────────────────────────
let _IS_ESP8266 = false;
let _IS_RP2040 = false;
let _rp2040Sim = null;
let _rp2040Running = false;
let _rp2040RafId = null;
let _rp2040UartBuf = '';
let _avrBoardType = 'board_uno';
let _esp8266SessionId = null;
let _esp8266EvtSource = null;
let _esp8266PinStates = {};
@@ -3064,7 +3123,12 @@ window.clearSerial = () => { document.getElementById('serial-output').innerHTML
window.sendSerial = () => {
const inp = document.getElementById('serial-input');
if (!inp.value || !uart) return;
if (!inp.value) return;
if (_IS_RP2040 && _rp2040Sim) {
for (const ch of inp.value + '\n') _rp2040Sim.rp2040.uart[0].feedByte(ch.charCodeAt(0));
inp.value = ''; return;
}
if (!uart) return;
for (const ch of inp.value + '\n') uart.writeByte(ch.charCodeAt(0));
inp.value = '';
};
@@ -3283,7 +3347,333 @@ window.stopEmulator = () => {
_updatePowerSwitch(); // 메시지 자동 출력
};
// ── RP2040 emulation ─────────────────────────────────────────────────────────
const _PICO_BOARD_SVG = `<svg id="board-svg" viewBox="0 0 90 200" xmlns="http://www.w3.org/2000/svg">
<!-- PCB -->
<rect x="1" y="1" width="88" height="198" rx="5" fill="#1a5c2a" stroke="#3a9a4a" stroke-width="1.2"/>
<!-- USB Micro-B -->
<rect x="31" y="0" width="28" height="10" rx="2" fill="#aaa" stroke="#ccc" stroke-width=".5"/>
<rect x="34" y="1.5" width="22" height="6" rx="1" fill="#666"/>
<text x="45" y="7.5" text-anchor="middle" fill="#eee" font-size="3.5" font-family="monospace">USB</text>
<!-- RP2040 chip -->
<rect x="22" y="78" width="46" height="44" rx="3" fill="#111" stroke="#555" stroke-width=".8"/>
<circle cx="24" cy="80" r="1.5" fill="#333"/>
<text x="45" y="97" text-anchor="middle" fill="#ccc" font-size="7" font-family="monospace" font-weight="bold">RP2040</text>
<text x="45" y="107" text-anchor="middle" fill="#888" font-size="3.5" font-family="monospace">Raspberry Pi</text>
<!-- Flash chip -->
<rect x="28" y="118" width="22" height="12" rx="1.5" fill="#222" stroke="#555" stroke-width=".6"/>
<text x="39" y="126" text-anchor="middle" fill="#777" font-size="3" font-family="monospace">FLASH</text>
<!-- Built-in LED GPIO25 -->
<circle id="pico-led" cx="76" cy="25" r="3.5" fill="#0a1f0a" stroke="#2a6a2a" stroke-width=".8"/>
<text x="76" y="20" text-anchor="middle" fill="#6aaa6a" font-size="3" font-family="monospace">LED</text>
<!-- BOOTSEL button -->
<rect x="58" y="148" width="14" height="7" rx="1.5" fill="#2a2a2a" stroke="#666" stroke-width=".5"/>
<text x="65" y="153.5" text-anchor="middle" fill="#aaa" font-size="3" font-family="monospace">BOOT</text>
<!-- Left pins (20) -->
${Array.from({length:20},(_,i)=>{
const y=18+i*9;
const labels=['VBUS','VSYS','GND','3V3','3V3_EN','ADC_REF','GP28','AGND','GP27','GP26','RUN','GP22','GND','GP21','GP20','GP19','GP18','GND','GP17','GP16'];
const isGnd=labels[i].includes('GND')||labels[i]==='AGND';
const is3v=labels[i].includes('3V3')||labels[i]==='VBUS'||labels[i]==='VSYS';
const col=isGnd?'#888':is3v?'#f80':'#5cf';
return `<rect x="1" y="${y}" width="6" height="5" rx="1" fill="#888" stroke="#aaa" stroke-width=".4"/>
<text x="9" y="${y+4}" fill="${col}" font-size="3" font-family="monospace">${labels[i]}</text>`;
}).join('')}
<!-- Right pins (20) -->
${Array.from({length:20},(_,i)=>{
const y=18+i*9;
const labels=['GP0','GP1','GND','GP2','GP3','GP4','GP5','GND','GP6','GP7','GP8','GP9','GND','GP10','GP11','GP12','GP13','GND','GP14','GP15'];
const isGnd=labels[i].includes('GND');
const col=isGnd?'#888':'#5cf';
return `<rect x="83" y="${y}" width="6" height="5" rx="1" fill="#888" stroke="#aaa" stroke-width=".4"/>
<text x="81" y="${y+4}" text-anchor="end" fill="${col}" font-size="3" font-family="monospace">${labels[i]}</text>`;
}).join('')}
</svg>`;
function _rp2040Start() {
if (_rp2040Running || !_rp2040Sim) return;
_rp2040Running = true;
const NS = 8; // 125 MHz → 8 ns/cycle
const rp2040 = _rp2040Sim.rp2040;
const clock = _rp2040Sim.clock;
const FLASH_BASE = 0x10000000, SRAM_BASE = 0x20000000;
// Uint16 뷰: Thumb 명령어(2바이트 정렬) 패턴 감지용
const flashU16 = new Uint16Array(rp2040.flash.buffer);
// Uint32 뷰: 4바이트 단위 bulk 복사/제로필 용
const flashU32 = new Uint32Array(rp2040.flash.buffer);
const sramU32 = new Uint32Array(rp2040.sram.buffer);
// SRAM 코드 fast-path 감지용 Uint16 뷰
const sramU16 = new Uint16Array(rp2040.sram.buffer);
// 범용 초기화 루프 fast-path 함수
// 패턴: STMIA Rn!,{r0} / CMP Rn,Rm / BNE or BCC back
function _tryFastLoop(pc, memU16, base, isFlash) {
const hOff = (pc - base) >>> 1;
const op0 = memU16[hOff], op1 = memU16[hOff+1], op2 = memU16[hOff+2];
// Fast-path A: LDMIA r1!,{r0}(c901) / STMIA r2!,{r0}(c201) / CMP r2,r3(429a) / BCC(d3fb)
if (isFlash && op0 === 0xc901 && op1 === 0xc201 && memU16[hOff+2] === 0x429a && memU16[hOff+3] === 0xd3fb) {
const r1 = rp2040.core.registers[1] >>> 0;
const r2 = rp2040.core.registers[2] >>> 0;
const r3 = rp2040.core.registers[3] >>> 0;
if (r2 < r3 && (r1 & 3) === 0 && (r2 & 3) === 0 &&
r1 >= FLASH_BASE && r1 < FLASH_BASE + rp2040.flash.length &&
r2 >= SRAM_BASE && r2 < SRAM_BASE + rp2040.sram.length) {
const cnt = (r3 - r2) >>> 2;
sramU32.set(flashU32.subarray((r1-FLASH_BASE)>>>2, ((r1-FLASH_BASE)>>>2)+cnt), (r2-SRAM_BASE)>>>2);
rp2040.core.registers[1] = r1 + cnt * 4;
rp2040.core.registers[2] = r3;
rp2040.core.PC = pc + 8;
clock.tick(NS * 2 * cnt * 4);
return true;
}
}
// Fast-path B: STMIA r1!,{r0}(c101) / CMP r1,r2(4291) / BNE(d1fc) — .bss 제로 루프
if (op0 === 0xc101 && op1 === 0x4291 && op2 === 0xd1fc) {
const r0 = rp2040.core.registers[0] >>> 0;
const r1 = rp2040.core.registers[1] >>> 0;
const r2 = rp2040.core.registers[2] >>> 0;
if (r1 < r2 && (r1 & 3) === 0 && (r2 & 3) === 0 &&
r1 >= SRAM_BASE && r2 <= SRAM_BASE + rp2040.sram.length) {
const cnt = (r2 - r1) >>> 2;
sramU32.fill(r0, (r1-SRAM_BASE)>>>2, ((r1-SRAM_BASE)>>>2)+cnt);
rp2040.core.registers[1] = r2;
rp2040.core.PC = pc + 6;
clock.tick(NS * 2 * cnt * 3);
return true;
}
}
return false;
}
// Stuck detection: same PC for too long usually means a fault/loop
let _lastPc = rp2040.core.PC, _stuckCycles = 0, _reportedStuck = false;
function tick() {
if (!_rp2040Running) return;
const _booted = !!(_rp2040Sim && _rp2040Sim._booted);
// 부팅 중: 10ms 버스트 후 즉시 재스케줄 (RAF 16ms 상한 없이 최대 처리량)
// 부팅 후: RAF로 전환 (UI 동기화)
const BURST = _booted ? 20 : 10; // ms
const deadline = performance.now() + BURST;
let checkN = 0;
try {
while (true) {
if (rp2040.core.waiting) {
const dt = clock.nanosToNextAlarm;
clock.tick(dt > 0 ? dt : NS);
} else {
const pc = rp2040.core.PC & ~1;
if (pc === _lastPc) {
if (++_stuckCycles > 50000 && !_reportedStuck) {
_reportedStuck = true;
appendSerial(`\n[경고: PC ${pc.toString(16)}에서 멈춤 — HardFault/idle loop 의심]\n`, 'error');
}
} else {
_lastPc = pc; _stuckCycles = 0; _reportedStuck = false;
}
// Flash 범위: fast-path 검사
if (pc >= FLASH_BASE && pc < FLASH_BASE + rp2040.flash.length - 8) {
if (_tryFastLoop(pc, flashU16, FLASH_BASE, true)) {
if (++checkN >= 500 && performance.now() >= deadline) break;
continue;
}
}
// SRAM 범위: fast-path 검사 (.bss 패턴만)
else if (pc >= SRAM_BASE && pc < SRAM_BASE + rp2040.sram.length - 6) {
if (_tryFastLoop(pc, sramU16, SRAM_BASE, false)) {
if (++checkN >= 500 && performance.now() >= deadline) break;
continue;
}
}
rp2040.step();
clock.tick(NS * 2);
}
if (++checkN >= 500 && performance.now() >= deadline) break;
}
} catch(e) { _rp2040Running = false; appendSerial('\n[실행 오류: ' + e.message + ']\n', 'error'); return; }
// Heartbeat: 부팅 중에는 1초마다, 부팅 후에는 3초마다 PC/SP/LR/waiting 출력
if (_rp2040Sim && _rp2040Sim._booted) {
_rp2040RafId = requestAnimationFrame(tick);
} else {
// 부팅 중: setTimeout(0)으로 최대한 빠르게 재스케줄
_rp2040RafId = setTimeout(tick, 0);
}
}
_rp2040RafId = setTimeout(tick, 0);
}
function _rp2040Stop() {
_rp2040Running = false;
if (_rp2040RafId) {
cancelAnimationFrame(_rp2040RafId);
clearTimeout(_rp2040RafId);
_rp2040RafId = null;
}
}
function _loadUF2(data, rp2040) {
const FLASH_START = 0x10000000;
for (let i = 0; i + 512 <= data.length; i += 512) {
const v = new DataView(data.buffer, data.byteOffset + i, 512);
if (v.getUint32(0, true) !== 0x0A324655 || v.getUint32(4, true) !== 0x9E5D5157) continue;
const addr = v.getUint32(12, true);
const size = v.getUint32(16, true);
const off = addr - FLASH_START;
if (off < 0 || off + size > rp2040.flash.length) continue;
rp2040.flash.set(new Uint8Array(data.buffer, data.byteOffset + i + 32, size), off);
}
}
async function initRP2040Mode(uf2B64, fname) {
_IS_RP2040 = true;
document.getElementById('filename').textContent = fname || 'sketch.ino';
document.querySelector('.board-title').textContent = 'RASPBERRY PI PICO · RP2040';
document.querySelector('.board-svg').innerHTML = _PICO_BOARD_SVG;
const footerEl = document.getElementById('footer-info');
if (footerEl) footerEl.textContent = 'rp2040js · RP2040 @ 125 MHz';
document.getElementById('btn-compile').style.display = 'none';
let Simulator, USBCDC;
try {
({ Simulator, USBCDC } = await import('https://esm.sh/rp2040js@1.3.3'));
} catch(e) { showOverlayError('rp2040js 로드 실패\n' + e.message); return; }
const uf2Bytes = Uint8Array.from(atob(uf2B64), c => c.charCodeAt(0));
const sim = new Simulator();
_rp2040Sim = sim;
// 1) RP2040 공식 bootrom 로드 (helper 함수 table을 위해 필수)
try {
const br = await fetch('/api/arduino/rp2040-bootrom', { credentials: 'include' });
if (!br.ok) throw new Error('HTTP ' + br.status);
const brData = await br.json();
const bootrom = Uint8Array.from(atob(brData.bin), c => c.charCodeAt(0));
if (bootrom.length !== 16 * 1024) throw new Error('bootrom size mismatch: ' + bootrom.length);
const bootromU32 = new Uint32Array(bootrom.buffer, bootrom.byteOffset, 4096);
sim.rp2040.loadBootrom(bootromU32);
appendSerial(`[RP2040 bootrom 로드 완료: ${bootrom.length} bytes]\n`, 'sys');
} catch(e) {
appendSerial('[RP2040 bootrom 로드 실패: ' + e.message + ']\n', 'error');
// bootrom 없이도 계속 시도 (helper 호출 시 멈출 수 있음)
}
// 2) UF2 → flash 로드
_loadUF2(uf2Bytes, sim.rp2040);
// 3) 앱 벡터 테이블 찾기
// Arduino-Pico UF2: boot2(0x10000000) 뒤에 임시 vector table(0x10000100)이 있고,
// 실제 .text 섹션 시작인 0x10003000에 진짜 vector table이 위치함.
// 보드/링커 설정에 따라 0x10003000 또는 0x10000100 중 하나를 사용.
const fv = new DataView(sim.rp2040.flash.buffer);
const readVTOR = (base) => ({ sp: fv.getUint32(base, true), pc: fv.getUint32(base + 4, true), base });
const candidates = [readVTOR(0x3000), readVTOR(0x100)].filter(v => v.sp && v.pc && (v.pc & ~1) >= 0x10000000);
const appVTOR = candidates[0];
if (!appVTOR) {
showOverlayError('UF2 로드 실패: 앱 벡터 테이블을 찾을 수 없습니다.\nrp2040:rp2040 코어(arduino-pico)를 사용하세요.');
return;
}
// 3) 부팅을 위해 VTOR를 찾은 앱 vector table로 설정.
// bootrom을 덮어쓰지 않으므로 bootrom helper 함수들은 그대로 사용 가능.
// reset()이 실행되면 core.PC = flash[VTOR+4], core.SP = flash[VTOR].
sim.rp2040.core.VTOR = 0x10000000 + appVTOR.base;
// 4) CPU 리셋 (VTOR → flash 앱 vector 테이블에서 SP/PC 로드)
sim.rp2040.core.reset();
// rp2040js 로거 경고 억제: 미구현 명령어·주변장치 warn이 콘솔에 범람하는 것을 방지
{
const logger = sim.rp2040.logger;
if (logger && typeof logger.warn === 'function') {
logger.warn = () => {}; // 모든 warn 억제 (error/info는 유지)
}
// core가 별도 logger를 가질 경우도 처리
const clogger = sim.rp2040.core?.logger;
if (clogger && clogger !== logger && typeof clogger.warn === 'function') {
clogger.warn = () => {};
}
}
// rp2040js 패치: exception(HardFault 등) 발생 시 실행을 멈추고 메시지를 출력.
// (주의: 이전 wide-Thumb 명령어 PC 패치는 BL/BLX 등 정상 32비트 명령어를 망가뜨려 제거함)
{
const core = sim.rp2040.core;
const _origException = core.exceptionEntry.bind(core);
core.exceptionEntry = function(exceptionNumber) {
if (exceptionNumber === 3) { // HardFault만 중지 (IRQ/SysTick/PendSV는 정상 실행)
_rp2040Running = false;
_origException(exceptionNumber);
appendSerial(`\n[HardFault (LR=${this.LR.toString(16)}, PC=${this.PC.toString(16)}) — 실행 중지]\n`, 'error');
} else {
_origException(exceptionNumber);
}
};
}
// Serial 출력 발생 시 부팅 완료로 전환 (setTimeout→RAF 스케줄러 전환)
function _markBooted() {
if (_rp2040Sim && !_rp2040Sim._booted) {
_rp2040Sim._booted = true;
appendSerial('[부팅 완료]\n', 'sys');
}
}
// UART0 → Serial1 (UTF-8 스트리밍 디코딩)
const _rp2040UartDec = new TextDecoder('utf-8', { fatal: false });
sim.rp2040.uart[0].onByte = (value) => {
_markBooted();
const decoded = _rp2040UartDec.decode(new Uint8Array([value]), { stream: true });
if (decoded) appendSerial(decoded);
};
// USB CDC → Serial (default Arduino-Pico Serial)
try {
const cdc = new USBCDC(sim.rp2040.usbCtrl);
const dec = new TextDecoder();
cdc.onSerialData = (buf) => { _markBooted(); appendSerial(dec.decode(buf)); };
cdc.onDeviceConnected = () => appendSerial('[USB CDC 연결됨]\n', 'sys');
} catch(e) {
appendSerial('[USB CDC 초기화 실패: ' + e.message + ']\n', 'error');
}
// GPIO25 = 내장 LED
sim.rp2040.gpio[25].addListener((state) => {
const on = state === 1;
const toolbar = document.getElementById('rp2040-led');
if (toolbar) toolbar.style.background = on ? '#4ade80' : '#1a2e1a';
const svg = document.getElementById('pico-led');
if (svg) svg.setAttribute('fill', on ? '#22c55e' : '#0a1f0a');
});
document.getElementById('overlay').style.display = 'none';
const ledEl2 = document.getElementById('rp2040-led');
if (ledEl2) ledEl2.style.display = 'inline-block';
document.getElementById('btn-stop').disabled = false;
setStatus('running', '실행 중');
const resetBtn = document.getElementById('btn-reset');
if (resetBtn) { resetBtn.disabled = false; }
appendSerial(`[RP2040 에뮬레이터 시작]\n[Serial → USB CDC, Serial1 → UART0]\n[VTOR=${sim.rp2040.core.VTOR.toString(16)} SP=${sim.rp2040.core.SP.toString(16)} PC=${sim.rp2040.core.PC.toString(16)}]\n`, 'sys');
_rp2040Start();
}
window.resetEmulator = () => {
if (_IS_RP2040) {
if (!_rp2040Sim) return;
_rp2040Stop();
_rp2040UartBuf = '';
appendSerial('\n--- 리셋 ---\n', 'sys');
// 주의: rp2040.reset()은 flash.fill(0xff)를 실행하므로 앱 코드가 날아감.
// VTOR를 다시 앱 vector table로 재설정한 뒤 core.reset()만 수행.
// (flash offset 0x3000 우선, 없으면 0x100)
const fv2 = new DataView(_rp2040Sim.rp2040.flash.buffer);
const readVTOR2 = (base) => ({ sp: fv2.getUint32(base, true), pc: fv2.getUint32(base + 4, true), base });
const cand2 = [readVTOR2(0x3000), readVTOR2(0x100)].filter(v => v.sp && v.pc && (v.pc & ~1) >= 0x10000000);
_rp2040Sim.rp2040.core.VTOR = cand2.length ? (0x10000000 + cand2[0].base) : 0x10003000;
_rp2040Sim.rp2040.core.reset();
_rp2040Start();
return;
}
if (!hexData) return;
// 먼저 실행 정지
running = false;
@@ -3357,6 +3747,11 @@ window.setTheme = (name) => {
};
window.togglePower = () => {
if (_IS_RP2040 && _rp2040Sim) {
if (_rp2040Running) { _rp2040Stop(); setStatus('stopped', '정지'); }
else { _rp2040Start(); setStatus('running', '실행 중'); }
return;
}
if (running) { stopEmulator(); return; }
if (!emulatorReady) return;
startEmulator();
@@ -3375,6 +3770,8 @@ async function boot() {
leonardo: 'ARDUINO LEONARDO · ATmega32U4', micro: 'ARDUINO MICRO · ATmega32U4',
esp32: 'ESP32 · Xtensa LX6', esp32s3: 'ESP32-S3 · Xtensa LX7',
esp8266: 'ESP8266 · L106',
rpipico: 'Raspberry Pi Pico · RP2040', rpipicow: 'Raspberry Pi Pico W · RP2040',
pico: 'Arduino Nano RP2040 Connect',
};
if (_fqbnBoard && _BOARD_NAMES[_fqbnBoard]) {
const boardTitleEl = document.querySelector('.board-title');
@@ -3408,6 +3805,19 @@ async function boot() {
return;
}
// ── RP2040 browser mode ──────────────────────────────────────────────────────
const uf2B64 = sessionStorage.getItem('arduino_uf2');
if (uf2B64) {
sessionStorage.removeItem('arduino_uf2');
const rp2040Sketch = sessionStorage.getItem('arduino_sketch');
sessionStorage.removeItem('arduino_sketch');
if (rp2040Sketch) _esp32SketchCode = rp2040Sketch;
const rp2040Fname = sessionStorage.getItem('arduino_filename') || 'sketch.ino';
sessionStorage.removeItem('arduino_filename');
initRP2040Mode(uf2B64, rp2040Fname);
return;
}
// ── Arduino AVR mode (기존) ──────────────────────────────────────────────────
const sketch = sessionStorage.getItem('arduino_sketch');
const board = sessionStorage.getItem('arduino_board') || 'uno';
@@ -3419,16 +3829,17 @@ async function boot() {
sessionStorage.removeItem('arduino_hex');
// AVR 모드 보드 타이틀 (URL fqbn이 없는 경우 sessionStorage board로 대체)
const _avrBoardKey = (_fqbnBoard || (board || 'uno').split(':').pop()).toLowerCase();
_avrBoardType = _avrBoardKey.includes('mega') ? 'board_mega' : _avrBoardKey.includes('nano') ? 'board_nano' : 'board_uno';
if (!_fqbnBoard) {
const _avrBoard = (board || 'uno').toLowerCase();
const _avrName = _BOARD_NAMES[_avrBoard] || _BOARD_NAMES.uno;
const _avrName = _BOARD_NAMES[_avrBoardKey] || _BOARD_NAMES.uno;
const boardTitleEl = document.querySelector('.board-title');
if (boardTitleEl && _avrBoard !== 'uno') boardTitleEl.textContent = _avrName;
if (boardTitleEl && _avrBoardKey !== 'uno') boardTitleEl.textContent = _avrName;
}
document.getElementById('filename').textContent = fname;
if (hexDirect) { initEmulator(hexDirect); return; }
if (hexDirect) { if (sketch) _esp32SketchCode = sketch; initEmulator(hexDirect); return; }
if (!sketch) {
showOverlayError('스케치가 없습니다.\n코드 에디터에서 .ino 파일을 열고 실행하세요.');
@@ -5659,8 +6070,7 @@ ${code.slice(0, 3000)}
// Auto-layout and apply generated component list to schematic
function _schApplyGenerated(boardType, components) {
if (!components.length) return;
if (SCH.components.length && !confirm(`회로도에 ${components.length}개 부품을 배치합니다. 기존 회로도를 지울까요?`)) return;
if (SCH.components.length && !confirm(components.length ? `회로도에 ${components.length}개 부품을 배치합니다. 기존 회로도를 지울까요?` : '회로도를 초기화하고 보드만 표시합니다. 계속할까요?')) return;
SCH.components = [];
SCH.wires = [];
@@ -5894,7 +6304,7 @@ function _schApplyGenerated(boardType, components) {
window.schGenFromCode = async function schGenFromCode() {
const code = _getEditorCode() || (_IS_ESP32 ? _esp32SketchCode : _esp8266SketchCode);
if (!code || !code.trim()) { alert('스케치 코드가 없습니다. 먼저 코드를 에디터에 붙여넣거나 컴파일하세요.'); return; }
const boardType = _IS_ESP32 ? 'board_esp32' : 'board_esp8266';
const boardType = _IS_ESP32 ? 'board_esp32' : _IS_ESP8266 ? 'board_esp8266' : _avrBoardType;
const btn = document.getElementById('sch-gen-btn');
if (btn) { btn.textContent = '⏳...'; btn.disabled = true; }
try {
@@ -5908,8 +6318,11 @@ window.schGenFromCode = async function schGenFromCode() {
else merged.push(aiComp);
}
const valid = merged.filter(c => SCH_TERMS[c.type]);
if (!valid.length) { alert('인식된 부품이 없습니다. 코드에 라이브러리 include나 pinMode 호출이 있는지 확인하세요.'); return; }
_schApplyGenerated(boardType, valid);
if (!valid.length) {
const log = document.getElementById('sch-log');
if (log) { log.textContent = '인식된 부품 없음 — 보드만 표시됩니다.'; log.style.color='#e8c860'; }
}
} finally {
if (btn) { btn.textContent = '⚡ 코드→회로'; btn.disabled = false; }
}
+16 -5
View File
@@ -16,13 +16,13 @@
<!-- CodeMirror + xterm (UMD libs loaded before Monaco's AMD loader to avoid anonymous define() clashes) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css">
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js"></script>
<link rel="stylesheet" href="/vendor/xterm.css">
<script src="/vendor/xterm.js"></script>
<script src="/vendor/addon-fit.js"></script>
<script src="/vendor/addon-web-links.js"></script>
<link rel="stylesheet" href="styles.css">
<!-- Monaco loader (AMD). Hide define.amd first so any stray UMD lib does not call Monaco's anonymous define(). -->
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.js"></script>
<script src="/vendor/vs/loader.js"></script>
<script>
try {
var _t = localStorage.getItem('code-page-theme') || localStorage.getItem('cherryclaw_theme') || 'dark';
@@ -510,6 +510,17 @@
</div>
<iframe id="code-preview-iframe" src="" style="flex:1;border:none;background:#fff;min-height:0" sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups"></iframe>
</div>
<!-- 🖥️ Emulator panel -->
<div id="code-emu-panel" style="display:none;flex-direction:column;flex-shrink:0;height:520px;min-height:120px;border-top:2px solid #1a4a25;background:#0a1410;position:relative">
<div id="code-emu-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
<div style="display:flex;align-items:center;height:32px;background:#0d1f18;border-bottom:1px solid #1a4a25;flex-shrink:0;padding:0 8px;gap:6px">
<span style="font-size:13px;flex-shrink:0">🖥️</span>
<span id="emu-panel-title" style="font-size:12px;font-weight:700;color:#4ade80;font-family:'IBM Plex Mono',monospace;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">에뮬레이터</span>
<button onclick="emuPanelNewTab()" title="새 탭에서 열기" style="background:none;border:none;color:#4a7a5c;cursor:pointer;font-size:13px;padding:0 5px;flex-shrink:0">⧉</button>
<button onclick="toggleEmuPanel()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:16px;padding:0 6px;line-height:1;flex-shrink:0">&times;</button>
</div>
<iframe id="code-emu-iframe" src="" style="flex:1;border:none;min-height:0" allow="serial;usb"></iframe>
</div>
<!-- ⚡ Upload panel -->
<div id="code-upload-panel" style="display:none;flex-direction:column;flex-shrink:0;height:300px;min-height:100px;border-top:2px solid #7a4e20;background:#1e1810;position:relative">
<div id="code-upload-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
+116 -44
View File
@@ -133,7 +133,7 @@ function initCodeView() {
setCodeAiTech(codeAiTech);
if (monacoReady) { setTimeout(() => { if (monacoEditor) monacoEditor.layout(); }, 50); return; }
if (typeof require === 'undefined') return;
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs' } });
require.config({ paths: { vs: '/vendor/vs' } });
require(['vs/editor/editor.main'], function() {
monacoReady = true;
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
@@ -909,80 +909,125 @@ function toggleCodePreview() {
}
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
}
let _emuPanelOpen = false;
let _emuPanelUrl = '';
function toggleEmuPanel() {
const panel = document.getElementById('code-emu-panel');
const btn = document.getElementById('code-emu-btn');
if (!panel) return;
_emuPanelOpen = !_emuPanelOpen;
panel.style.display = _emuPanelOpen ? 'flex' : 'none';
if (btn) btn.classList.toggle('active', _emuPanelOpen);
if (!_emuPanelOpen) {
const iframe = document.getElementById('code-emu-iframe');
if (iframe) iframe.src = '';
_emuPanelUrl = '';
}
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
}
function _emuPanelLoad(url, title) {
const panel = document.getElementById('code-emu-panel');
const iframe = document.getElementById('code-emu-iframe');
const lbl = document.getElementById('emu-panel-title');
const btn = document.getElementById('code-emu-btn');
if (!panel || !iframe) return;
_emuPanelUrl = url;
iframe.src = url;
if (lbl) lbl.textContent = title || '에뮬레이터';
if (!_emuPanelOpen) {
_emuPanelOpen = true;
panel.style.display = 'flex';
if (btn) btn.classList.add('active');
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
}
}
function emuPanelNewTab() {
if (_emuPanelUrl) window.open(_emuPanelUrl, '_blank');
}
function openArduinoEmulator() {
const file = codeFiles.find(f => f.id === activeCodeFileId);
const btn = document.getElementById('code-emu-btn');
if (!file || !file.name.endsWith('.ino')) { window.open('/arduino-emulator.html', 'arduino-emulator'); return; }
const btn = document.getElementById('code-emu-btn');
const content = file.model ? file.model.getValue() : (file.content || '');
const fqbn = _csbSelBoard.fqbn;
if (!file || !file.name.endsWith('.ino')) {
_emuPanelLoad('/arduino-emulator.html', '에뮬레이터');
return;
}
const content = file.model ? file.model.getValue() : (file.content || '');
const fqbn = _csbSelBoard.fqbn;
const fqbnArch = fqbn.split(':')[1] || '';
if (fqbnArch === 'esp32') {
// ESP32 계열 → QEMU (DIO 모드로 서버가 직접 컴파일)
// 팝업 차단 방지: 클릭 직후 즉시 창 열고, 완료 후 URL 교체
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
_emuPanelLoad('/arduino-emulator.html?loading=1', `ESP32 — ${file.name}`);
codeAiAppend('system', `🖥️ ESP32 에뮬레이터 컴파일 중... [${_csbSelBoard.name}]`);
// Store sketch code so emulator can show it (localStorage: shared across windows)
try { localStorage.setItem('esp32_sketch', content); } catch {}
api('/api/arduino/qemu/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ code: content, fqbn }) })
.then(d => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
if (d.error) {
emuWin.close();
const isUnsupported = d.error.includes('미지원') || d.error.includes('RISC-V');
if (isUnsupported) codeAiAppend('system', '⚠️ ' + d.error);
else sendCompileErrorToAI(d.error, file.name, content);
toggleEmuPanel();
return;
}
const _proj = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
const url = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
_emuPanelLoad(url, `ESP32 — ${file.name}`);
codeAiAppend('system', `✅ ESP32 에뮬레이터 실행 중.`);
}).catch(e => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ ' + e.message);
emuWin.close();
toggleEmuPanel();
});
} else if (fqbnArch === 'esp8266') {
// ESP8266 → Native Linux x86 컴파일 + 실행
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
_emuPanelLoad('/arduino-emulator.html?loading=1', `ESP8266 — ${file.name}`);
codeAiAppend('system', `🖥️ ESP8266 에뮬레이터 컴파일 중... [${_csbSelBoard.name}]`);
try { localStorage.setItem('esp8266_sketch', content); } catch {}
api('/api/arduino/esp8266/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ code: content, fqbn }) })
.then(d => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
if (d.error) {
emuWin.close();
sendCompileErrorToAI(d.error, file.name, content);
toggleEmuPanel();
return;
}
const _proj = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
const url = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
_emuPanelLoad(url, `ESP8266 — ${file.name}`);
codeAiAppend('system', `✅ ESP8266 에뮬레이터 실행 중.`);
}).catch(e => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ ' + e.message);
emuWin.close();
toggleEmuPanel();
});
} else {
// AVR 및 기타 → 기존 컴파일 → AVR 에뮬레이터 or 다운로드
// AVR 및 기타
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
fetch('/api/arduino/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders({}) },
body: JSON.stringify({ code: content, board: fqbn, fqbn }),
}).then(r => r.json()).then(data => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
if (data.error) { sendCompileErrorToAI(data.error, file.name, content); return; }
if (data.hex) {
sessionStorage.setItem('arduino_hex', data.hex);
const w = window.open('/arduino-emulator.html', 'arduino-emulator');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
sessionStorage.setItem('arduino_sketch', content);
_emuPanelLoad('/arduino-emulator.html', `AVR — ${file.name}`);
} else if (data.boardType === 'rp2040') {
sessionStorage.setItem('arduino_uf2', data.bin);
sessionStorage.setItem('arduino_sketch', content);
_emuPanelLoad('/arduino-emulator.html', `RP2040 — ${file.name}`);
} else if (data.bin) {
const ext = data.boardType === 'uf2' ? '.uf2' : '.bin';
const blob = new Blob([Uint8Array.from(atob(data.bin), c => c.charCodeAt(0))], { type: 'application/octet-stream' });
@@ -992,7 +1037,7 @@ function openArduinoEmulator() {
codeAiAppend('system', `✅ 컴파일 완료 (${(data.size/1024).toFixed(1)} KB) — ${ext.slice(1).toUpperCase()} 다운로드됨.\n보드에 올리려면 ⚡ 업로드 버튼을 사용하세요.`);
}
}).catch(e => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ 서버 오류: ' + e.message);
});
}
@@ -1409,33 +1454,31 @@ function codeRunWith(mode) {
}
if (mode === 'esp32') {
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
_emuPanelLoad('/arduino-emulator.html?loading=1', `ESP32 — ${file.name}`);
codeAiAppend('system', `🖥️ ESP32 에뮬레이터 컴파일 중... (${file.name})`);
try { localStorage.setItem('esp32_sketch', file.content); } catch {}
api('/api/arduino/qemu/start', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ code: file.content, fqbn: 'esp32:esp32:esp32' }) })
.then(d => {
if (d.error) { sendCompileErrorToAI(d.error, file.name, file.content); emuWin.close(); return; }
if (d.error) { sendCompileErrorToAI(d.error, file.name, file.content); toggleEmuPanel(); return; }
const _proj2 = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=esp32%3Aesp32%3Aesp32${_proj2}`;
_emuPanelLoad(`/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=esp32%3Aesp32%3Aesp32${_proj2}`, `ESP32 — ${file.name}`);
codeAiAppend('system', `✅ ESP32 에뮬레이터 실행 중.`);
}).catch(e => { codeAiAppend('system', '⚠️ ' + e.message); emuWin.close(); });
}).catch(e => { codeAiAppend('system', '⚠️ ' + e.message); toggleEmuPanel(); });
return;
}
if (mode === 'esp8266') {
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
_emuPanelLoad('/arduino-emulator.html?loading=1', `ESP8266 — ${file.name}`);
codeAiAppend('system', `🖥️ ESP8266 에뮬레이터 컴파일 중... (${file.name})`);
try { localStorage.setItem('esp8266_sketch', file.content); } catch {}
const _esp8266Fqbn = (_csbSelBoard.fqbn.startsWith('esp8266:')) ? _csbSelBoard.fqbn : 'esp8266:esp8266:nodemcuv2';
api('/api/arduino/esp8266/start', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ code: file.content, fqbn: _esp8266Fqbn }) })
.then(d => {
if (d.error) { sendCompileErrorToAI(d.error, file.name, file.content); emuWin.close(); return; }
if (d.error) { sendCompileErrorToAI(d.error, file.name, file.content); toggleEmuPanel(); return; }
const _proj2 = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(_esp8266Fqbn)}${_proj2}`;
_emuPanelLoad(`/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(_esp8266Fqbn)}${_proj2}`, `ESP8266 — ${file.name}`);
codeAiAppend('system', `✅ ESP8266 에뮬레이터 실행 중.`);
}).catch(e => { codeAiAppend('system', '⚠️ ' + e.message); emuWin.close(); });
}).catch(e => { codeAiAppend('system', '⚠️ ' + e.message); toggleEmuPanel(); });
return;
}
@@ -1466,10 +1509,13 @@ function codeRunWith(mode) {
// (이미 위에서 처리됨, fallback)
codeAiAppend('system', `✅ 컴파일 완료.`);
} else if (data.hex) {
// AVR/ESP8266 → 에뮬레이터
sessionStorage.setItem('arduino_hex', data.hex);
const w = window.open('/arduino-emulator.html', '_blank');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
sessionStorage.setItem('arduino_sketch', file.content);
_emuPanelLoad('/arduino-emulator.html', `AVR — ${file.name}`);
} else if (data.boardType === 'rp2040') {
sessionStorage.setItem('arduino_uf2', data.bin);
sessionStorage.setItem('arduino_sketch', file.content);
_emuPanelLoad('/arduino-emulator.html', `RP2040 — ${file.name}`);
} else if (data.bin) {
const ext = data.boardType === 'uf2' ? '.uf2' : '.bin';
const blob = new Blob([Uint8Array.from(atob(data.bin), c => c.charCodeAt(0))], { type: 'application/octet-stream' });
@@ -5878,6 +5924,7 @@ function cupOpenWith(file) {
cupLog(`방식: ${_cupMethodLabel(method)}`, 'info');
if (method === 'stm32-uart') cupLog('STM32: BOOT0=HIGH 후 RESET → ⚡ 업로드', 'warn');
else if (method === 'avr-stk500') cupLog('AVR: 포트 선택 후 ⚡ 업로드 (자동 DTR 리셋)', 'info');
else if (method === 'rp2040-emulator') cupLog('RP2040: ⚡ 업로드 버튼으로 에뮬레이터 실행 / ⬇ 다운로드로 UF2 파일 저장', 'info');
else if (method === 'download-uf2' || method === 'download-bin') cupLog('이 보드는 ⬇ 다운로드 후 수동 복사 방식입니다.', 'warn');
else if (method === 'esp32-flash') cupLog('ESP32/ESP8266: 🔌 포트 선택 후 ⚡ 업로드\n 컴파일: 서버 | 플래싱: 브라우저 esptool-js', 'info');
if (!navigator.serial) cupLog('⚠ WebSerial 미지원 브라우저입니다. Chrome/Edge를 사용하세요.', 'warn');
@@ -5926,13 +5973,13 @@ function _cupGetUploadMethod(fqbn) {
if (arch === 'avr') return 'avr-stk500';
if (vendor === 'STMicroelectronics') return 'stm32-uart';
if (arch === 'samd' || arch === 'sam') return 'download-bin';
if (arch === 'mbed_rp2040' || arch === 'rp2040') return 'download-uf2';
if (arch === 'mbed_rp2040' || arch === 'rp2040') return 'rp2040-emulator';
if (arch === 'esp32' || arch === 'esp8266') return 'esp32-flash';
return 'download-bin';
}
function _cupMethodLabel(m) {
return { 'avr-stk500':'STK500 (WebSerial)', 'stm32-uart':'STM32 UART', 'download-bin':'.bin 다운로드', 'download-uf2':'UF2 드래그&드롭', 'esp32-flash':'esptool-js (브라우저)' }[m] || m;
return { 'avr-stk500':'STK500 (WebSerial)', 'stm32-uart':'STM32 UART', 'download-bin':'.bin 다운로드', 'download-uf2':'UF2 드래그&드롭', 'rp2040-emulator':'RP2040 에뮬레이터', 'esp32-flash':'esptool-js (브라우저)' }[m] || m;
}
function _cupGetEffectiveFqbn() {
@@ -6024,7 +6071,7 @@ async function cupDoDownload() {
_cupCompiledData = data;
const isHex = !!data.hex;
const content = isHex ? data.hex : _cupBase64ToBytes(data.bin || data.merged);
const ext = data.boardType === 'uf2' ? '.uf2' : (isHex ? '.hex' : '.bin');
const ext = (data.boardType === 'uf2' || data.boardType === 'rp2040') ? '.uf2' : (isHex ? '.hex' : '.bin');
const blob = new Blob([content], { type: isHex ? 'text/plain' : 'application/octet-stream' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
@@ -6039,6 +6086,16 @@ async function cupDoUpload() {
cupTab('log');
const method = _cupGetUploadMethod(_cupGetEffectiveFqbn());
if (method === 'esp32-flash') { await _cupUploadESP32(); return; }
if (method === 'rp2040-emulator') {
let data;
if (_cupCacheHit()) { data = _cupCompiledData; cupLog('캐시된 컴파일 결과 사용', 'info'); }
else { data = await _cupCompile(); if (!data) return; _cupCompiledData = data; }
if (!data) return;
sessionStorage.setItem('arduino_uf2', data.bin);
sessionStorage.setItem('arduino_sketch', _cupCode || '');
_emuPanelLoad('/arduino-emulator.html', `RP2040 — ${_cupFname}`);
return;
}
if (method === 'download-uf2' || method === 'download-bin') { await cupDoDownload(); return; }
_cupBusy = true;
document.getElementById('cup-btn-upload').disabled = true;
@@ -6297,6 +6354,21 @@ async function cupMonSend() {
function cupMonClear() { const el=document.getElementById('cup-mon-output'); if(el) el.innerHTML=''; _cupMonBuf=''; }
// Emulator panel resize drag
document.addEventListener('DOMContentLoaded', function() {
const drag = document.getElementById('code-emu-drag');
const panel = document.getElementById('code-emu-panel');
if (!drag || !panel) return;
let startY=0, startH=0;
drag.addEventListener('mousedown', e => {
startY=e.clientY; startH=panel.offsetHeight; e.preventDefault();
const onMove = e2 => { const dy=startY-e2.clientY; panel.style.height=Math.max(120,Math.min(window.innerHeight*.75,startH+dy))+'px'; };
const onUp = () => { document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); drag.style.background=''; };
drag.addEventListener('mouseover', ()=>drag.style.background='rgba(34,197,94,.4)');
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
});
});
// Upload panel resize drag
document.addEventListener('DOMContentLoaded', function() {
const drag = document.getElementById('code-upload-drag');