From 18b9d5ba7410233764af1f5898e80c62743e2d05 Mon Sep 17 00:00:00 2001 From: kim Date: Sun, 14 Jun 2026 20:12:05 +0900 Subject: [PATCH] =?UTF-8?q?v3.0.1:=20=EC=97=90=EB=AE=AC=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=E2=80=94=20=EC=A0=95=EC=A7=80/=EC=9E=AC=EC=8B=9C=EC=9E=91,=20U?= =?UTF-8?q?TF-8=20=EC=8B=9C=EB=A6=AC=EC=96=BC,=20CodeMirror,=20=EC=86=8D?= =?UTF-8?q?=EB=8F=84=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=8D=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 에디터 (code.html / code.js) - xterm → @xterm/xterm 5.5.0 마이그레이션 (UMD 전역명 AddonFit/AddonWebLinks) - Monaco Editor 0.52.0 업그레이드 - AI 패널 CSS 변수 (--ai-hdr-bg 등) 다크/라이트 테마 분리 - Arduino/GCC/Python/JS 오류 패턴(_ERROR_PATTERNS) 확장 Arduino 에뮬레이터 (arduino-emulator.html) - CodeMirror 5 스케치 에디터 내장 (C++ 구문 강조, BroadcastChannel 동기화) - 속도 select → range 슬라이더 (1x~최대, 로그 스케일) - 시리얼 UTF-8: 수동 바이트버퍼 → 스트리밍 TextDecoder({ stream:true })로 교체 (°C 깨짐 수정) - 정지/시작 토글 버튼 추가 (AVR: togglePower, ESP32/ESP8266: pause↔restart) - 회로→코드 버튼 (🤖) 추가 - 보드 타이틀 FQBN 기반 동적 설정 서버 (server-v2.ts) - QEMU/ESP8266 restart 엔드포인트: old PTY exit 핸들러가 새 세션을 삭제하던 레이스컨디션 수정 (s.pty = null 가드) - ESP8266 /pause 엔드포인트 추가 - 정지 시 DELETE → pause로 변경 (세션·firmware 유지, 재시작 가능) Co-Authored-By: Claude Sonnet 4.6 --- src/gateway/server-v2.ts | 3267 ++++++++++++++++++- web-ui/arduino-emulator.html | 5944 ++++++++++++++++++++++++++++++---- web-ui/code.html | 43 +- web-ui/code.js | 389 ++- 4 files changed, 8964 insertions(+), 679 deletions(-) diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts index 80bd9ab..8231839 100644 --- a/src/gateway/server-v2.ts +++ b/src/gateway/server-v2.ts @@ -9342,6 +9342,95 @@ app.get('/code-serve/{*filePath}', (req: express.Request, res: express.Response) } }); +// ── Schematic (회로도) 저장 API ─────────────────────────────────────────────── +const DEFAULT_SCHEMATIC_DIR = path.resolve(__dirname, '../../web-ui/schematics'); +if (!fs.existsSync(DEFAULT_SCHEMATIC_DIR)) fs.mkdirSync(DEFAULT_SCHEMATIC_DIR, { recursive: true }); + +// project 이름으로 저장 디렉토리 해석 (워크스페이스/project) +function resolveSchematicDir(project: string): string { + if (!project) return DEFAULT_SCHEMATIC_DIR; + const workspace = getConfig().getWorkspacePath(); + const targetDir = path.join(workspace, project); + if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); + return targetDir; +} + +// 회로도 목록 가져오기 +app.get('/api/schematics', (req: express.Request, res: express.Response) => { + try { + const project = typeof req.query.project === 'string' ? req.query.project : ''; + if (!project) { res.json({ files: [] }); return; } + const targetDir = resolveSchematicDir(project); + if (!fs.existsSync(targetDir)) { res.json({ files: [] }); return; } + const files = fs.readdirSync(targetDir) + .filter(f => f.endsWith('.json')) + .map(f => { + const stat = fs.statSync(path.join(targetDir, f)); + return { name: f, size: stat.size, mtime: stat.mtime.toISOString() }; + }) + .sort((a, b) => b.mtime.localeCompare(a.mtime)); + res.json({ files }); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}); + +// 회로도 저장 +app.post('/api/schematics', (req: express.Request, res: express.Response) => { + try { + const { name, data, project } = req.body; + if (!name || !data) { res.status(400).json({ error: '이름과 데이터가 필요합니다' }); return; } + if (!project) { res.status(400).json({ error: '프로젝트가 필요합니다' }); return; } + const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_'); + const fileName = safeName.endsWith('.json') ? safeName : safeName + '.json'; + const targetDir = resolveSchematicDir(project); + const filePath = path.join(targetDir, fileName); + const content = { ...data, savedAt: new Date().toISOString() }; + fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8'); + res.json({ success: true, fileName, project }); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}); + +// 회로도 불러오기 +app.get('/api/schematics/:fileName', (req: express.Request, res: express.Response) => { + try { + const rawFileName = req.params.fileName; + const rawProject = req.query.project; + const fileName = Array.isArray(rawFileName) ? rawFileName[0] : (rawFileName || ''); + const project = typeof rawProject === 'string' ? rawProject : ''; + if (!project) { res.status(400).json({ error: '프로젝트가 필요합니다' }); return; } + const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_'); + const targetDir = resolveSchematicDir(project); + const filePath = path.join(targetDir, safeName); + if (!fs.existsSync(filePath)) { res.status(404).json({ error: '파일이 없습니다' }); return; } + const content = fs.readFileSync(filePath, 'utf-8'); + res.json(JSON.parse(content)); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}); + +// 회로도 삭제 +app.delete('/api/schematics/:fileName', (req: express.Request, res: express.Response) => { + try { + const rawFileName = req.params.fileName; + const rawProject = req.query.project; + const fileName = Array.isArray(rawFileName) ? rawFileName[0] : (rawFileName || ''); + const project = typeof rawProject === 'string' ? rawProject : ''; + if (!project) { res.status(400).json({ error: '프로젝트가 필요합니다' }); return; } + const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_'); + const targetDir = resolveSchematicDir(project); + const filePath = path.join(targetDir, safeName); + if (!fs.existsSync(filePath)) { res.status(404).json({ error: '파일이 없습니다' }); return; } + fs.unlinkSync(filePath); + res.json({ success: true }); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}); + app.get('/api/files/{*filePath}', (req: express.Request, res: express.Response) => { try { @@ -14242,6 +14331,128 @@ app.delete('/api/arduino/libs/uninstall', async (req, res) => { ); }); +// ── Chip detection & QEMU info ─────────────────────────────────────────────── +function _detectChip(fqbn: string): string { + const parts = fqbn.split(':'); + const vendor = parts[0] || ''; + const bid = (parts[2] || '').toLowerCase(); + if (vendor === 'esp8266') return 'ESP8266'; + if (vendor === 'arduino') return 'AVR'; + const s2kw = ['esp32s2','magtag','funhouse','atmegazero_esp32s2']; + if (s2kw.some(k => bid.includes(k)) || /_s2[_-]/.test(bid) || bid.endsWith('_s2')) return 'S2'; + const s3kw = ['esp32s3','nano_nora','nora_w10']; + if (s3kw.some(k => bid.includes(k)) || bid.endsWith('s3') || /[-_]s3r/.test(bid) || /_s3[-_]/.test(bid)) return 'S3'; + if (bid.includes('esp32c3') || bid.includes('airm2m') || /_c3[_-]/.test(bid) || bid.endsWith('_c3')) return 'C3'; + if (bid.includes('esp32c5') || /_c5[_-]/.test(bid) || bid.endsWith('_c5')) return 'C5'; + if (bid.includes('esp32c6') || /_c6[_-]/.test(bid) || bid.endsWith('_c6')) return 'C6'; + if (bid.includes('esp32h2') || /_h2[_-]/.test(bid) || bid.endsWith('_h2')) return 'H2'; + if (bid.includes('esp32p4') || /_p4[_-]/.test(bid) || bid.endsWith('_p4')) return 'P4'; + return 'ESP32'; +} +function _qemuForChip(chip: string): string | false { + if (chip === 'ESP32') return 'esp32'; + if (chip === 'S3') return 'esp32s3'; + if (chip === 'ESP8266') return 'esp8266-native'; + return false; +} + +// ESP32 계열 대표 보드 화이트리스트 (검색 없을 때만 적용) +const _ESP32_FEATURED: Record = { + // ── 원본 ESP32 (QEMU: esp32) ────────────────── + 'esp32:esp32:esp32': 'ESP32 Dev Module', + 'esp32:esp32:esp32wrover': 'ESP32 Wrover Module', + 'esp32:esp32:esp32cam': 'AI Thinker ESP32-CAM', + 'esp32:esp32:nodemcu-32s': 'NodeMCU-32S', + 'esp32:esp32:lolin32': 'WEMOS LOLIN32', + 'esp32:esp32:adafruit_feather_esp32_v2':'Adafruit Feather ESP32 V2', + 'esp32:esp32:esp32thing_plus': 'SparkFun ESP32 Thing Plus', + // ── ESP32-S3 (QEMU: esp32s3) ───────────────── + 'esp32:esp32:esp32s3': 'ESP32-S3 Dev Module', + 'esp32:esp32:nano_nora': 'Arduino Nano ESP32', + 'esp32:esp32:adafruit_feather_esp32s3': 'Adafruit Feather ESP32-S3', + 'esp32:esp32:lolin_s3': 'LOLIN S3', + 'esp32:esp32:XIAO_ESP32S3': 'Seeed XIAO ESP32-S3', + // ── ESP32-S2 ───────────────────────────────── + 'esp32:esp32:esp32s2': 'ESP32-S2 Dev Module', + 'esp32:esp32:lolin_s2_mini': 'LOLIN S2 Mini', + // ── ESP32-C3 ───────────────────────────────── + 'esp32:esp32:esp32c3': 'ESP32-C3 Dev Module', + 'esp32:esp32:lolin_c3_mini': 'LOLIN C3 Mini', + // ── ESP32-C6 ───────────────────────────────── + 'esp32:esp32:esp32c6': 'ESP32-C6 Dev Module', + // ── ESP32-H2 ───────────────────────────────── + 'esp32:esp32:esp32h2': 'ESP32-H2 Dev Module', + // ── M5Stack ────────────────────────────────── + 'esp32:esp32:m5stack_core2': 'M5Stack Core2', + 'esp32:esp32:m5stack_cores3': 'M5Stack CoreS3', + 'esp32:esp32:m5stack_atom': 'M5Stack ATOM', + 'esp32:esp32:m5stack_stickc_plus2': 'M5Stack StickC Plus2', +}; + +// ESP8266 대표 보드 (3개 브랜드로 통합) +const _ESP8266_FEATURED: Record = { + 'esp8266:esp8266:nodemcuv2': 'NodeMCU v2 (ESP-12E)', + 'esp8266:esp8266:d1_mini': 'WEMOS D1 Mini', + 'esp8266:esp8266:esp01': 'ESP-01 Wi-Fi 모듈', +}; + +// ── Arduino board listall (FQBN 목록) ──────────────────────────────────────── +app.get('/api/arduino/boards/listall', async (req, res) => { + const sess = getSessionUser(req); + if (!sess) return res.status(401).json({ error: 'Not authenticated' }); + const cli = await getArduinoCli(); + if (!cli) return res.status(503).json({ error: 'arduino-cli not found' }); + const q = String(req.query.q || '').trim().toLowerCase(); + const execP = (cmd: string) => new Promise((resolve) => + require('child_process').exec(cmd, { timeout: 30000, maxBuffer: 64 * 1024 * 1024 }, (_e: any, out: string) => resolve(out || ''))); + const out = await execP(`"${cli}" board search "" --format json`); + try { + const data = JSON.parse(out || '{}'); + type BoardEntry = { fqbn: string; name: string; chip: string; qemu: string | false; variants: number }; + const allBoards: BoardEntry[] = (data.boards || []) + .filter((b: any) => b.fqbn) + .map((b: any) => { + const fqbn = (b.fqbn || '').trim(); + const name = (b.name || '').trim(); + const chip = _detectChip(fqbn); + return { fqbn, name, chip, qemu: _qemuForChip(chip), variants: 1 }; + }); + + let boards: BoardEntry[]; + if (q) { + // 검색: 전체에서 매칭 + boards = allBoards.filter(b => b.name.toLowerCase().includes(q) || b.fqbn.toLowerCase().includes(q)); + } else { + // 기본 목록: ESP32/ESP8266은 화이트리스트만, 나머지(AVR)는 전체 + const esp32Featured = Object.entries(_ESP32_FEATURED) + .map(([fqbn, label]) => { + const found = allBoards.find(b => b.fqbn === fqbn); + if (!found) return null; + return { ...found, name: label }; + }) + .filter(Boolean) as BoardEntry[]; + const esp8266Featured = Object.entries(_ESP8266_FEATURED) + .map(([fqbn, label]) => { + const found = allBoards.find(b => b.fqbn === fqbn); + if (!found) return null; + return { ...found, name: label }; + }) + .filter(Boolean) as BoardEntry[]; + const others = allBoards.filter(b => !b.fqbn.startsWith('esp32:') && !b.fqbn.startsWith('esp8266:')); + boards = [...esp32Featured, ...esp8266Featured, ...others]; + } + + // 정렬: esp32:esp32:esp32 최상단 고정, 나머지 알파벳순 + boards.sort((a, b) => { + if (a.fqbn === 'esp32:esp32:esp32') return -1; + if (b.fqbn === 'esp32:esp32:esp32') return 1; + return a.name.localeCompare(b.name); + }); + + res.json({ boards }); + } catch { res.json({ boards: [] }); } +}); + // ── Arduino board (core) management ────────────────────────────────────────── app.get('/api/arduino/cores', async (req, res) => { const sess = getSessionUser(req); @@ -14275,7 +14486,7 @@ app.get('/api/arduino/cores/search', async (req, res) => { const cli = await getArduinoCli(); if (!cli) return res.status(503).json({ error: 'arduino-cli not found' }); const execP = (cmd: string) => new Promise((resolve) => - require('child_process').exec(cmd, { timeout: 30000 }, (_e: any, out: string) => resolve(out || ''))); + require('child_process').exec(cmd, { timeout: 30000, maxBuffer: 32 * 1024 * 1024 }, (_e: any, out: string) => resolve(out || ''))); const searchQ = q ? `"${q.replace(/"/g,'')}"` : '""'; const [searchOut, listOut] = await Promise.all([ execP(`"${cli}" core search ${searchQ} --format json`), @@ -14357,14 +14568,87 @@ app.get('/api/arduino/libs/example', async (req, res) => { }); }); +// ── ngspice API ─────────────────────────────────────────────────────────────── +app.post('/api/spice/run', async (req, res) => { + const sess = getSessionUser(req); + if (!sess) return res.status(401).json({ error: 'Not authenticated' }); + + const { netlist, analysis = '.op' } = req.body as { netlist?: string; analysis?: string }; + if (!netlist) return res.status(400).json({ error: 'netlist is required' }); + + const { execFile } = require('child_process') as typeof import('child_process'); + const fs = require('fs') as typeof import('fs'); + const os = require('os') as typeof import('os'); + const path = require('path') as typeof import('path'); + + // Check ngspice is installed + const ngspicePath: string = await new Promise(resolve => { + execFile('which', ['ngspice'], (err: any, stdout: string) => resolve(err ? '' : stdout.trim())); + }); + if (!ngspicePath) return res.status(503).json({ error: 'ngspice not installed. Run: sudo apt install ngspice' }); + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spice-')); + const inputFile = path.join(tmpDir, 'circuit.cir'); + + // Build complete ngspice netlist (title line required as first line) + const fullNetlist = `* SmallClaw SPICE\n${netlist.trim()}\n${analysis}\n.end\n`; + fs.writeFileSync(inputFile, fullNetlist); + + try { + const stdout: string = await new Promise((resolve, reject) => { + execFile(ngspicePath, ['-b', inputFile], { timeout: 15000 }, + (err: any, stdout: string, stderr: string) => { + const out = (stdout + '\n' + stderr).trim(); + if (err && !out.includes('No. of Data Rows')) reject(new Error(out || err.message)); + else resolve(out); + } + ); + }); + + // Parse ngspice batch output: + // Node voltages appear in a table: + // Node Voltage + // ---- ------- + // nodename 3.333333e+00 + // Branch currents: + // Source Current + // ------ ------- + // v1#branch -1.66667e-03 + const voltages: Record = {}; + const currents: Record = {}; + const lines = stdout.split('\n'); + let mode: 'none' | 'voltage' | 'current' = 'none'; + for (const line of lines) { + if (/Node\s+Voltage/i.test(line)) { mode = 'voltage'; continue; } + if (/Source\s+Current/i.test(line)) { mode = 'current'; continue; } + if (/^\s*[-=]{3,}/.test(line)) continue; // separator + if (/^\s*$/.test(line)) { mode = 'none'; continue; } + if (mode === 'voltage') { + const m = line.match(/^\s+(\S+)\s+([-\d.eE+]+)\s*$/); + if (m) voltages[m[1].toLowerCase()] = parseFloat(m[2]); + } else if (mode === 'current') { + const m = line.match(/^\s+(\S+)\s+([-\d.eE+]+)\s*$/); + if (m) currents[m[1].toLowerCase().replace('#branch','')] = parseFloat(m[2]); + } + } + + res.json({ ok: true, voltages, currents, log: stdout }); + } catch (err: any) { + res.status(500).json({ error: err.message || 'ngspice failed' }); + } finally { + try { fs.rmSync(tmpDir, { recursive: true }); } catch {} + } +}); + app.post('/api/arduino/compile', async (req, res) => { const sess = getSessionUser(req); if (!sess) return res.status(401).json({ error: 'Not authenticated' }); - const { code, board = 'uno' } = req.body as { code?: string; board?: string }; + const { code, board = 'uno', fqbn: reqFqbn } = req.body as { code?: string; board?: string; fqbn?: string }; if (!code) return res.status(400).json({ error: 'code is required' }); - const isESP32 = board.startsWith('esp32'); - const fqbn = isESP32 ? 'esp32:esp32:esp32' + const isESP32 = board.startsWith('esp32') || (reqFqbn || '').startsWith('esp32:'); + const fqbn = reqFqbn && /^[\w\-:\.]+$/.test(reqFqbn) ? reqFqbn + : isESP32 ? 'esp32:esp32:esp32' : board === 'nano' ? 'arduino:avr:nano:cpu=atmega328old' : 'arduino:avr:uno'; @@ -14396,7 +14680,13 @@ app.post('/api/arduino/compile', async (req, res) => { fs.mkdirSync(sketchDir); fs.writeFileSync(path.join(sketchDir, `${sketchName}.ino`), code); - type CompileResult = { hex?: string; merged?: string; size?: number; error?: string; autoInstalled?: string[] }; + // FQBN 기반 보드 계열 판별 + const fqbnVendor = fqbn.split(':')[0] || ''; + const fqbnArch = fqbn.split(':')[1] || ''; + // AVR 계열만 .hex 생성 (esp8266은 xtensa → .bin) + const producesHex = fqbnArch === 'avr'; + + type CompileResult = { hex?: string; merged?: string; bin?: string; size?: number; boardType?: string; error?: string; autoInstalled?: string[] }; const runCompile = (): Promise => new Promise((resolve) => { require('child_process').exec( @@ -14405,15 +14695,38 @@ app.post('/api/arduino/compile', async (req, res) => { (err: any, stdout: string, stderr: string) => { if (err) { resolve({ error: stderr || stdout || String(err) }); return; } const files = fs.readdirSync(tmpDir); - if (isESP32) { + // ESP32 계열(arch=esp32): merged.bin 우선 + if (fqbnArch === 'esp32' || isESP32) { const mergedFile = files.find((f: string) => f.endsWith('.merged.bin')); - if (!mergedFile) { resolve({ error: 'ESP32 컴파일 완료 but merged.bin 없음\n' + stdout }); return; } - const buf = fs.readFileSync(path.join(tmpDir, mergedFile)); - resolve({ merged: buf.toString('base64'), size: buf.length }); - } else { + if (mergedFile) { + const buf = fs.readFileSync(path.join(tmpDir, mergedFile)); + resolve({ merged: buf.toString('base64'), size: buf.length, boardType: 'esp32' }); + return; + } + } + // AVR 계열: .hex + if (producesHex) { const hexFile = files.find((f: string) => f.endsWith('.hex')); - if (!hexFile) { resolve({ error: '컴파일 완료 but .hex 파일 없음\n' + stdout }); return; } - resolve({ hex: fs.readFileSync(path.join(tmpDir, hexFile), 'utf-8') }); + if (hexFile) { + resolve({ hex: fs.readFileSync(path.join(tmpDir, hexFile), 'utf-8'), boardType: 'avr' }); + return; + } + } + // 그 외(STM32, RP2040, SAMD 등): .bin 또는 .uf2 + const binFile = files.find((f: string) => f.endsWith('.bin') && !f.endsWith('.merged.bin')); + const uf2File = files.find((f: string) => f.endsWith('.uf2')); + const hexFile = files.find((f: string) => f.endsWith('.hex')); + if (binFile) { + const buf = fs.readFileSync(path.join(tmpDir, binFile)); + const bType = fqbnArch === 'esp8266' ? 'esp8266' : 'bin'; + resolve({ bin: buf.toString('base64'), size: buf.length, boardType: bType }); + } else if (uf2File) { + const buf = fs.readFileSync(path.join(tmpDir, uf2File)); + resolve({ bin: buf.toString('base64'), size: buf.length, boardType: 'uf2' }); + } else if (hexFile) { + resolve({ hex: fs.readFileSync(path.join(tmpDir, hexFile), 'utf-8'), boardType: 'hex' }); + } else { + resolve({ error: `컴파일 완료 but 출력 파일 없음\n생성 파일: ${files.join(', ')}\n${stdout}` }); } } ); @@ -14423,6 +14736,15 @@ app.post('/api/arduino/compile', async (req, res) => { new Promise(r => require('child_process').exec(cmd, { timeout: ms }, (_: any, o: string) => r(o || ''))); try { + // ── Auto-install missing core ───────────────────────────────────────────── + const coreId = fqbn.split(':').slice(0, 2).join(':'); // e.g. "STMicroelectronics:stm32" + const installedOut = await execQ(`"${cliPath}" core list --format json`); + const installedIds: string[] = JSON.parse(installedOut || '{}')?.platforms?.map((p: any) => p.id) || []; + if (coreId && !installedIds.includes(coreId)) { + console.log(`[arduino] auto-installing core: ${coreId}`); + await execQ(`"${cliPath}" core install "${coreId}"`, 300000); + } + let result = await runCompile(); // ── Auto-install missing libraries and re-compile ───────────────────────── @@ -14460,6 +14782,2927 @@ app.post('/api/arduino/compile', async (req, res) => { } }); +// ── ESP8266 Native emulator sessions ────────────────────────────────────────── +interface Esp8266Session { + proc: any | null; tmpDir: string; binPath: string; fqbn: string; + listeners: Set<(d: string) => void>; + sseConns: Set; + timer: ReturnType; + ptyBuf: string; + gpioBuf: string[]; +} +const esp8266Sessions = new Map(); + +// gpio_bridge_esp8266.h — Arduino API stubs for native Linux x86 compilation +const _GPIO_BRIDGE_ESP8266_H = `#pragma once +#ifndef _ESP8266_EMU_BRIDGE +#define _ESP8266_EMU_BRIDGE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ── Arduino integer types ────────────────────────────────────────────────────── +typedef uint8_t byte; +typedef bool boolean; +typedef uint8_t uint8; +typedef int8_t int8; +typedef uint16_t uint16; +typedef int16_t int16; +typedef uint32_t uint32; +typedef int32_t int32; + +// ── Constants ────────────────────────────────────────────────────────────────── +#define HIGH 1 +#define LOW 0 +#define INPUT 0 +#define OUTPUT 1 +#define INPUT_PULLUP 2 +#define OUTPUT_OPEN_DRAIN 3 +#define PI 3.14159265358979323846f +#define TWO_PI 6.28318530718f +#define LSBFIRST 0 +#define MSBFIRST 1 +#define CHANGE 1 +#define FALLING 2 +#define RISING 3 +#define NOINTERRUPT -1 + +// NodeMCU D-pin → GPIO mapping +#define D0 16 +#define D1 5 +#define D2 4 +#define D3 0 +#define D4 2 +#define D5 14 +#define D6 12 +#define D7 13 +#define D8 15 +#define D9 3 +#define D10 1 +#define RX 3 +#define TX 1 +#define LED_BUILTIN 2 + +// PROGMEM / Flash string no-ops on Linux +#define PROGMEM +#define ICACHE_FLASH_ATTR +#define ICACHE_RAM_ATTR +#define F(x) (x) +#define PSTR(x) (x) +#define pgm_read_byte(addr) (*(const uint8_t*)(addr)) +#define pgm_read_word(addr) (*(const uint16_t*)(addr)) +#define pgm_read_dword(addr) (*(const uint32_t*)(addr)) +#define memcpy_P memcpy +#define strlen_P strlen +#define strcmp_P strcmp +#define strcpy_P strcpy + +// ── Math helpers ─────────────────────────────────────────────────────────────── +#define min(a,b) ((a)<(b)?(a):(b)) +#define max(a,b) ((a)>(b)?(a):(b)) +#define abs(x) ((x)>0?(x):-(x)) +#define constrain(x,lo,hi) ((x)<(lo)?(lo):(x)>(hi)?(hi):(x)) +#define round(x) ((x)>=0?(long)((x)+0.5):(long)((x)-0.5)) +#define radians(deg) ((deg)*PI/180.0f) +#define degrees(rad) ((rad)*180.0f/PI) +#define sq(x) ((x)*(x)) + +static inline long map(long x, long in_min, long in_max, long out_min, long out_max) { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} +static inline float mapf(float x, float a, float b, float c, float d) { + return (x - a) * (d - c) / (b - a) + c; +} + +// ── Time ─────────────────────────────────────────────────────────────────────── +static struct timeval _emu_start_tv; +static bool _emu_start_set = false; +static inline void _emu_init_time() { + if (!_emu_start_set) { gettimeofday(&_emu_start_tv, nullptr); _emu_start_set = true; } +} +static inline unsigned long millis() { + _emu_init_time(); + struct timeval tv; gettimeofday(&tv, nullptr); + return (unsigned long)((tv.tv_sec - _emu_start_tv.tv_sec) * 1000UL + (tv.tv_usec - _emu_start_tv.tv_usec) / 1000); +} +static inline unsigned long micros() { + _emu_init_time(); + struct timeval tv; gettimeofday(&tv, nullptr); + return (unsigned long)((tv.tv_sec - _emu_start_tv.tv_sec) * 1000000UL + (tv.tv_usec - _emu_start_tv.tv_usec)); +} +static inline void delay(unsigned long ms) { usleep((useconds_t)(ms * 1000UL)); } +static inline void delayMicroseconds(unsigned int us) { usleep((useconds_t)us); } +static inline void yield() {} + +// ── GPIO event protocol ──────────────────────────────────────────────────────── +namespace _emb8266 { + static inline void pin(int p, int v) { printf("\\x02G%d:%d\\n", p, v); fflush(stdout); } + static inline void mode(int p, char m) { printf("\\x02M%d:%c\\n", p, m); fflush(stdout); } + + // DHT mock state — updated via \\x02DSET:::\\n on stdin + static float _dht_t[20]; + static float _dht_h[20]; + static bool _dht_init_done = false; + // I2C / 1-Wire / motor state + static float _imu_values[128][7]; + static bool _imu_init_done = false; + static float _bme_values[128][3]; + static bool _bme_init_done = false; + static float _ow_t[20]; + static bool _ow_init_done = false; + static int _ow_last_pin = 0; + static int _motor_values[6] = {-1,-1,-1,-1,-1,-1}; + static char _cmd_buf[96]; + static int _cmd_len = 0; + static pthread_mutex_t _dht_mu = PTHREAD_MUTEX_INITIALIZER; + + static void _dht_init_defaults() { + if (_dht_init_done) return; + _dht_init_done = true; + for (int i=0;i<20;i++){_dht_t[i]=25.0f;_dht_h[i]=50.0f;} + } + static void _sensor_init_defaults() { + if (!_imu_init_done) { + _imu_init_done = true; + for (int a=0;a<128;a++) for (int v=0;v<7;v++) _imu_values[a][v]=0; + for (int a=0;a<128;a++) { _imu_values[a][2]=1.0f; _imu_values[a][6]=25.0f; } + } + if (!_bme_init_done) { + _bme_init_done = true; + for (int a=0;a<128;a++) { _bme_values[a][0]=25.0f; _bme_values[a][1]=50.0f; _bme_values[a][2]=1013.0f; } + } + if (!_ow_init_done) { + _ow_init_done = true; + for (int i=0;i<20;i++) _ow_t[i]=25.0f; + } + } + + // Parse host commands from stdin (called by Serial stdin reader thread) + static void _parse_stdin_cmd(const char* cmd) { + pthread_mutex_lock(&_dht_mu); + _sensor_init_defaults(); + float t, h, p; int pin, addr; + if (sscanf(cmd, "DSET:%d:%f:%f", &pin, &t, &h) == 3 && pin >= 0 && pin < 20) { + _dht_t[pin] = t; _dht_h[pin] = h; + } else if (sscanf(cmd, "ISET:%d:%f:%f:%f:%f:%f:%f:%f", &addr, + &_imu_values[addr][0], &_imu_values[addr][1], &_imu_values[addr][2], + &_imu_values[addr][3], &_imu_values[addr][4], &_imu_values[addr][5], + &_imu_values[addr][6]) == 8 && addr >= 0 && addr < 128) { + // parsed + } else if (sscanf(cmd, "BSET:%d:%f:%f:%f", &addr, &t, &h, &p) == 4 && addr >= 0 && addr < 128) { + _bme_values[addr][0]=t; _bme_values[addr][1]=h; _bme_values[addr][2]=p; + } else if (sscanf(cmd, "OSET:%d:%f", &pin, &t) == 2 && pin >= 0 && pin < 20) { + _ow_t[pin]=t; + } else if (sscanf(cmd, "MSET:%d:%d:%d:%d:%d:%d", + &_motor_values[0], &_motor_values[1], &_motor_values[2], + &_motor_values[3], &_motor_values[4], &_motor_values[5]) == 6) { + // parsed + } + pthread_mutex_unlock(&_dht_mu); + } + + static void _on_stdin_byte(char c) { + if (c == '\\x02') { _cmd_len = 0; return; } + if (c == '\\n' && _cmd_len > 0) { + _cmd_buf[_cmd_len] = 0; + _parse_stdin_cmd(_cmd_buf); + _cmd_len = 0; + return; + } + if (_cmd_len < 95) _cmd_buf[_cmd_len++] = c; + } + + static void _dht_poll() { + pthread_mutex_lock(&_dht_mu); + // values are updated in-place by stdin thread, nothing to do + pthread_mutex_unlock(&_dht_mu); + } +} + +// ── String class ─────────────────────────────────────────────────────────────── +class String { + char* _buf; int _len; int _cap; + void _ensure(int n) { + if (n < _cap) return; + int nc = n + 32; + char* nb = (char*)realloc(_buf, nc); + if (!nb) return; + _buf = nb; _cap = nc; + } +public: + String() : _buf((char*)calloc(1,1)), _len(0), _cap(1) {} + String(const char* s) : _buf(nullptr), _len(0), _cap(0) { + _buf = (char*)calloc(1,1); _cap = 1; + if (s) { int l = strlen(s); _ensure(l+1); memcpy(_buf, s, l+1); _len = l; } + } + String(const String& o) : _buf(nullptr), _len(0), _cap(0) { + _buf = (char*)calloc(1,1); _cap = 1; + _ensure(o._len+1); memcpy(_buf, o._buf, o._len+1); _len = o._len; + } + String(char c) : _buf((char*)calloc(2,1)), _len(1), _cap(2) { _buf[0]=c; } + String(int n, int=10) : _buf(nullptr), _len(0), _cap(0) { + char tmp[32]; snprintf(tmp,32,"%d",n); + _buf=(char*)calloc(1,1); _cap=1; int l=strlen(tmp); _ensure(l+1); memcpy(_buf,tmp,l+1); _len=l; + } + String(unsigned int n, int=10) : _buf(nullptr), _len(0), _cap(0) { + char tmp[32]; snprintf(tmp,32,"%u",n); + _buf=(char*)calloc(1,1); _cap=1; int l=strlen(tmp); _ensure(l+1); memcpy(_buf,tmp,l+1); _len=l; + } + String(long n, int=10) : _buf(nullptr), _len(0), _cap(0) { + char tmp[32]; snprintf(tmp,32,"%ld",n); + _buf=(char*)calloc(1,1); _cap=1; int l=strlen(tmp); _ensure(l+1); memcpy(_buf,tmp,l+1); _len=l; + } + String(unsigned long n, int=10) : _buf(nullptr), _len(0), _cap(0) { + char tmp[32]; snprintf(tmp,32,"%lu",n); + _buf=(char*)calloc(1,1); _cap=1; int l=strlen(tmp); _ensure(l+1); memcpy(_buf,tmp,l+1); _len=l; + } + String(double n, int dec=2) : _buf(nullptr), _len(0), _cap(0) { + char tmp[48]; snprintf(tmp,48,"%.*f",dec,n); + _buf=(char*)calloc(1,1); _cap=1; int l=strlen(tmp); _ensure(l+1); memcpy(_buf,tmp,l+1); _len=l; + } + ~String() { free(_buf); } + String& operator=(const String& o) { + if (this==&o) return *this; + _ensure(o._len+1); memcpy(_buf, o._buf, o._len+1); _len=o._len; return *this; + } + String& operator=(const char* s) { + if (!s) { _buf[0]=0; _len=0; return *this; } + int l=strlen(s); _ensure(l+1); memcpy(_buf,s,l+1); _len=l; return *this; + } + String operator+(const String& o) const { + String r(_buf); r._ensure(r._len+o._len+1); memcpy(r._buf+r._len, o._buf, o._len+1); r._len+=o._len; return r; + } + String operator+(const char* s) const { return *this + String(s); } + String& operator+=(const String& o) { *this=*this+o; return *this; } + String& operator+=(const char* s) { *this=*this+String(s); return *this; } + String& operator+=(char c) { *this=*this+String(c); return *this; } + bool operator==(const String& o) const { return strcmp(_buf, o._buf)==0; } + bool operator==(const char* s) const { return s && strcmp(_buf, s)==0; } + bool operator!=(const String& o) const { return !(*this==o); } + bool operator!=(const char* s) const { return !(*this==s); } + bool operator<(const String& o) const { return strcmp(_buf,o._buf)<0; } + bool operator>(const String& o) const { return strcmp(_buf,o._buf)>0; } + char operator[](int i) const { return (i>=0&&i<_len)?_buf[i]:0; } + char& operator[](int i) { static char z=0; return (i>=0&&i<_len)?_buf[i]:z; } + int length() const { return _len; } + bool isEmpty() const { return _len==0; } + const char* c_str() const { return _buf; } + bool startsWith(const char* p) const { return p && strncmp(_buf, p, strlen(p))==0; } + bool startsWith(const String& s) const { return startsWith(s._buf); } + bool endsWith(const char* p) const { + if (!p) return false; int pl=strlen(p); return pl<=_len && strcmp(_buf+_len-pl, p)==0; + } + bool endsWith(const String& s) const { return endsWith(s._buf); } + int indexOf(char c, int from=0) const { + for (int i=from;i<_len;i++) if (_buf[i]==c) return i; return -1; + } + int indexOf(const char* s, int from=0) const { + if (!s) return -1; char* p=strstr(_buf+from, s); return p?((int)(p-_buf)):-1; + } + int indexOf(const String& s, int from=0) const { return indexOf(s._buf, from); } + int lastIndexOf(char c) const { + for (int i=_len-1;i>=0;i--) if (_buf[i]==c) return i; return -1; + } + String substring(int s, int e=-1) const { + if (s<0) s=0; if (e<0||e>_len) e=_len; + if (s>=e) return String(); + char tmp[e-s+1]; memcpy(tmp,_buf+s,e-s); tmp[e-s]=0; return String(tmp); + } + void remove(int idx, int cnt=1) { + if (idx<0||idx>=_len) return; + if (idx+cnt>_len) cnt=_len-idx; + memmove(_buf+idx, _buf+idx+cnt, _len-idx-cnt+1); _len-=cnt; + } + void replace(char from, char to) { for(int i=0;i<_len;i++) if(_buf[i]==from) _buf[i]=to; } + void trim() { + int s=0,e=_len; + while(ss&&(_buf[e-1]==' '||_buf[e-1]=='\\t'||_buf[e-1]=='\\n'||_buf[e-1]=='\\r'))e--; + if(s>0) memmove(_buf,_buf+s,e-s+1); _len=e-s; _buf[_len]=0; + } + void toUpperCase() { for(int i=0;i<_len;i++) if(_buf[i]>='a'&&_buf[i]<='z')_buf[i]-=32; } + void toLowerCase() { for(int i=0;i<_len;i++) if(_buf[i]>='A'&&_buf[i]<='Z')_buf[i]+=32; } + int toInt() const { return atoi(_buf); } + float toFloat() const { return (float)atof(_buf); } + double toDouble() const { return atof(_buf); } + bool equals(const String& o) const { return *this==o; } + bool equals(const char* s) const { return *this==s; } + bool equalsIgnoreCase(const String& o) const { return strcasecmp(_buf,o._buf)==0; } + String& concat(const String& s) { *this+=s; return *this; } + String& concat(const char* s) { *this+=String(s); return *this; } + String& concat(char c) { *this+=c; return *this; } + String& concat(int n) { *this+=String(n); return *this; } +}; +static inline String operator+(const char* a, const String& b) { return String(a) + b; } + +// ── HardwareSerial ───────────────────────────────────────────────────────────── +class HardwareSerial { + static const int BUFSIZE = 512; + char _rbuf[BUFSIZE]; + int _rhead, _rtail; + pthread_mutex_t _mu; + pthread_t _reader; + static void* _read_thread(void* self_) { + HardwareSerial* self = (HardwareSerial*)self_; + while (true) { + char c; + ssize_t n = ::read(STDIN_FILENO, &c, 1); + if (n <= 0) { usleep(10000); continue; } + // Pass control bytes to command parser + _emb8266::_on_stdin_byte(c); + // Also buffer printable bytes for Serial.read() + if ((unsigned char)c >= 32 || c == '\\n' || c == '\\r' || c == '\\t') { + pthread_mutex_lock(&self->_mu); + int next = (self->_rtail + 1) % BUFSIZE; + if (next != self->_rhead) { self->_rbuf[self->_rtail] = c; self->_rtail = next; } + pthread_mutex_unlock(&self->_mu); + } + } + return nullptr; + } +public: + HardwareSerial() : _rhead(0), _rtail(0) { pthread_mutex_init(&_mu, nullptr); } + void begin(long baud = 115200) { + (void)baud; + pthread_create(&_reader, nullptr, _read_thread, this); + } + operator bool() const { return true; } + bool available() { return false; } // overridden below + int available_() { + pthread_mutex_lock(&_mu); + int n = (_rtail - _rhead + BUFSIZE) % BUFSIZE; + pthread_mutex_unlock(&_mu); + return n; + } + int read() { + pthread_mutex_lock(&_mu); + if (_rhead == _rtail) { pthread_mutex_unlock(&_mu); return -1; } + char c = _rbuf[_rhead]; _rhead = (_rhead + 1) % BUFSIZE; + pthread_mutex_unlock(&_mu); + return (unsigned char)c; + } + int peek() { + pthread_mutex_lock(&_mu); + int c = (_rhead == _rtail) ? -1 : (unsigned char)_rbuf[_rhead]; + pthread_mutex_unlock(&_mu); + return c; + } + void flush() { fflush(stdout); } + size_t write(uint8_t b) { putchar(b); fflush(stdout); return 1; } + size_t write(const char* s, size_t n) { + for (size_t i=0;i=0;i--,n>>=1) tmp[31-i]='0'+(n&1); tmp[32]=0; snprintf(buf,32,"%s",tmp+strspn(tmp,"0")?tmp+strspn(tmp,"0"):"0"); } + else snprintf(buf,32,"%d",n); + return print(buf); + } + size_t print(unsigned int n, int base=10) { return print((int)n, base); } + size_t print(long n, int base=10) { char buf[32]; snprintf(buf,32,"%ld",n); return print(buf); } + size_t print(unsigned long n, int base=10) { char buf[32]; snprintf(buf,32,"%lu",n); return print(buf); } + size_t print(float n, int dec=2) { char buf[32]; snprintf(buf,32,"%.*f",dec,n); return print(buf); } + size_t print(double n, int dec=2) { char buf[32]; snprintf(buf,32,"%.*f",dec,n); return print(buf); } + size_t print(bool n) { return print(n?"1":"0"); } + template size_t println(T v, int b=10) { size_t n=print(v,b); putchar('\\n'); fflush(stdout); return n+1; } + size_t println() { putchar('\\n'); fflush(stdout); return 1; } + size_t println(const char* s) { size_t n=print(s); putchar('\\n'); fflush(stdout); return n+1; } + size_t println(const String& s) { return println(s.c_str()); } + size_t println(float n, int dec=2) { size_t r=print(n,dec); putchar('\\n'); fflush(stdout); return r+1; } + size_t println(double n, int dec=2){ size_t r=print(n,dec); putchar('\\n'); fflush(stdout); return r+1; } + size_t println(bool n) { size_t r=print(n); putchar('\\n'); fflush(stdout); return r+1; } + String readStringUntil(char delim) { + String r; + for (int i=0;i<200;i++) { // max 200 chars + unsigned long t0=millis(); + while (available_()==0 && millis()-t0<1000) usleep(5000); + int c=read(); if(c<0) break; + if((char)c==delim) break; + r+=String((char)c); + } + return r; + } + String readString() { + String r; unsigned long t0=millis(); + while(millis()-t0<1000){ if(available_()) r+=String((char)read()); else usleep(5000); } + return r; + } +}; +// Redefine available() as a method that checks the buffer +#define available() available_() +extern HardwareSerial Serial; +extern HardwareSerial Serial1; + +// ── GPIO functions ───────────────────────────────────────────────────────────── +static inline void pinMode(uint8_t pin, uint8_t mode) { + _emb8266::mode((int)pin, mode==OUTPUT?'O':'I'); +} +static inline void digitalWrite(uint8_t pin, uint8_t val) { + _emb8266::pin((int)pin, val?1:0); +} +static inline int digitalRead(uint8_t pin) { (void)pin; return 0; } +static inline int analogRead(uint8_t pin) { (void)pin; return 0; } +static inline void analogWrite(uint8_t pin, int val) { (void)pin; (void)val; } +static inline void analogWriteFreq(uint32_t freq) { (void)freq; } +static inline void analogWriteRange(uint32_t range) { (void)range; } +static inline void tone(uint8_t pin, unsigned int freq, unsigned long dur=0) { (void)pin;(void)freq;(void)dur; } +static inline void noTone(uint8_t pin) { (void)pin; } +static inline void shiftOut(uint8_t data, uint8_t clk, uint8_t order, uint8_t val) { (void)data;(void)clk;(void)order;(void)val; } +static inline uint32_t shiftIn(uint8_t data, uint8_t clk, uint8_t order) { (void)data;(void)clk;(void)order; return 0; } +static inline unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout=1000000UL) { (void)pin;(void)state;(void)timeout; return 0; } + +// ── Interrupts (no-op) ───────────────────────────────────────────────────────── +typedef void (*voidFuncPtr)(); +static inline void attachInterrupt(uint8_t pin, voidFuncPtr fn, int mode) { (void)pin;(void)fn;(void)mode; } +static inline void detachInterrupt(uint8_t pin) { (void)pin; } +static inline void interrupts() {} +static inline void noInterrupts() {} + +// ── randomSeed / random ──────────────────────────────────────────────────────── +static inline void randomSeed(unsigned long s) { srand((unsigned int)s); } +static inline long random(long max) { return (long)(rand() % (max>0?max:1)); } +static inline long random(long min, long max) { return min + (long)(rand() % (max>min?max-min:1)); } + +// ── Bit operations ───────────────────────────────────────────────────────────── +#define bitRead(val,n) (((val)>>(n))&1) +#define bitSet(val,n) ((val)|=(1<<(n))) +#define bitClear(val,n) ((val)&=~(1<<(n))) +#define bitWrite(val,n,b) ((b)?bitSet(val,n):bitClear(val,n)) +#define bit(n) (1<<(n)) +#define lowByte(w) ((uint8_t)((w)&0xFF)) +#define highByte(w) ((uint8_t)(((w)>>8)&0xFF)) +#define word(h,l) ((uint16_t)((h)<<8|(l))) + +// ── ESP8266 system class ─────────────────────────────────────────────────────── +class ESPClass { +public: + void restart() { exit(0); } + void reset() { exit(0); } + uint32_t getFreeHeap() { return 40000; } + uint32_t getChipId() { return 0x00deadbeef & 0xFFFFFF; } + uint32_t getFlashChipId() { return 0xFAFAFAFA; } + uint32_t getFlashChipSize() { return 4*1024*1024; } + uint32_t getCpuFreqMHz() { return 80; } + String getResetReason() { return String("Power on"); } + String getResetInfo() { return String("Power on"); } + uint32_t getFreeContStack() { return 4096; } + bool deepSleep(uint32_t us, int mode=0) { (void)us;(void)mode; usleep(100000); return true; } + void wdtEnable(uint32_t t=0) { (void)t; } + void wdtDisable() {} + void wdtFeed() {} +}; +extern ESPClass ESP; + +// ── EEPROM stub ──────────────────────────────────────────────────────────────── +class EEPROMClass { + uint8_t _data[4096] = {}; + int _size = 0; +public: + void begin(int size) { _size = size < 4096 ? size : 4096; } + uint8_t read(int addr) { return (addr >= 0 && addr < _size) ? _data[addr] : 0; } + void write(int addr, uint8_t val) { if (addr >= 0 && addr < _size) _data[addr] = val; } + bool commit() { return true; } + void end() {} + uint8_t* getDataPtr() { return _data; } +}; +extern EEPROMClass EEPROM; + +// ── Wire (I2C) stub ──────────────────────────────────────────────────────────── +class TwoWire { +public: + void begin() {} + void begin(uint8_t addr) { (void)addr; } + void begin(int sda, int scl) { (void)sda;(void)scl; } + void setClock(uint32_t) {} + void setClockStretchLimit(uint32_t) {} + void beginTransmission(uint8_t addr) { (void)addr; } + uint8_t endTransmission(bool=true) { return 0; } + uint8_t requestFrom(uint8_t addr, uint8_t qty, bool=true) { (void)addr;(void)qty; return 0; } + size_t write(uint8_t b) { (void)b; return 1; } + size_t write(const uint8_t* d, size_t n) { (void)d; return n; } + int available() { return 0; } + int read() { return -1; } + int peek() { return -1; } + void flush() {} + void onReceive(void(*)(int)) {} + void onRequest(void(*)(void)) {} +}; +extern TwoWire Wire; + +// ── SPI stub ────────────────────────────────────────────────────────────────── +#define SPI_HAS_TRANSACTION 1 +#define SPI_MODE0 0 +#define SPI_MODE1 1 +#define SPI_MODE2 2 +#define SPI_MODE3 3 +class SPISettings { +public: + SPISettings(uint32_t=4000000, uint8_t=MSBFIRST, uint8_t=SPI_MODE0) {} +}; +class SPIClass { + static void _spiPins(int sck,int miso,int mosi,int ss){ + _emb8266::mode(sck,'O');_emb8266::mode(miso,'I');_emb8266::mode(mosi,'O');_emb8266::mode(ss,'O'); + _emb8266::pin(sck,0);_emb8266::pin(mosi,0);_emb8266::pin(ss,1); + } +public: + void begin(){ _spiPins(14,12,13,15); } + void begin(int8_t sck,int8_t miso,int8_t mosi,int8_t ss=-1){ + _spiPins(sck<0?14:sck, miso<0?12:miso, mosi<0?13:mosi, ss<0?15:ss); + } + void end() {} + void beginTransaction(SPISettings) {} + void endTransaction() {} + uint8_t transfer(uint8_t d) { return d; } + uint16_t transfer16(uint16_t d) { return d; } + void transfer(void*, size_t) {} + void setBitOrder(uint8_t) {} + void setDataMode(uint8_t) {} + void setClockDivider(uint32_t) {} + bool initialized() { return true; } +}; +extern SPIClass SPI; + +// ── Ticker stub ─────────────────────────────────────────────────────────────── +class Ticker { +public: + template void attach(float, F) {} + template void attach_ms(uint32_t, F) {} + template void attach(float, F, A) {} + void detach() {} + bool active() { return false; } +}; + +// ── LittleFS / SPIFFS stub ──────────────────────────────────────────────────── +struct File { operator bool() { return false; } }; +class FSClass { +public: + bool begin() { return false; } + bool begin(bool) { return false; } + void end() {} + File open(const char* p, const char* m = nullptr) { (void)p; (void)m; return {}; } + bool exists(const char*) { return false; } + bool remove(const char*) { return false; } + bool rename(const char*, const char*) { return false; } + bool mkdir(const char*) { return false; } + bool rmdir(const char*) { return false; } +}; +extern FSClass LittleFS; +extern FSClass SPIFFS; + +// ── WiFi stub ───────────────────────────────────────────────────────────────── +#define WIFI_STA 1 +#define WIFI_AP 2 +#define WIFI_AP_STA 3 +#define WL_CONNECTED 3 +#define WL_DISCONNECTED 6 +#define WL_CONNECT_FAILED 4 +#define WL_NO_SHIELD 255 +class IPAddress { + uint8_t _d[4]; +public: + IPAddress(uint8_t a=0,uint8_t b=0,uint8_t c=0,uint8_t d=0){_d[0]=a;_d[1]=b;_d[2]=c;_d[3]=d;} + IPAddress(uint32_t v){_d[0]=(v)&0xFF;_d[1]=(v>>8)&0xFF;_d[2]=(v>>16)&0xFF;_d[3]=(v>>24)&0xFF;} + String toString() const { + char buf[20]; snprintf(buf,20,"%d.%d.%d.%d",_d[0],_d[1],_d[2],_d[3]); return String(buf); + } + bool operator==(const IPAddress& o) const { return memcmp(_d,o._d,4)==0; } + uint8_t operator[](int i) const { return _d[i]; } +}; +class WiFiClient { +public: + bool connect(const char*, uint16_t) { return false; } + bool connect(IPAddress, uint16_t) { return false; } + bool connected() { return false; } + void stop() {} + int available() { return 0; } + int read() { return -1; } + int read(uint8_t*, size_t) { return 0; } + size_t write(uint8_t) { return 0; } + size_t write(const uint8_t*, size_t n) { return n; } + void flush() {} + void setTimeout(int) {} + String readStringUntil(char) { return String(); } + operator bool() { return false; } +}; +class WiFiServer { +public: + WiFiServer(uint16_t) {} + void begin() {} + WiFiClient available() { return {}; } +}; +class ESP8266WiFiClass { + String _ssid; + String _pass; + int _status = WL_DISCONNECTED; + IPAddress _localIP; + IPAddress _gw; + IPAddress _mask; + int _rssi = -99; + bool _connSent = false; + void _emitState() { + printf("\\x02WIFI:{\\"connected\\":%s,\\"ssid\\":\\"%s\\",\\"ip\\":\\"%s\\",\\"rssi\\":%d}\\n", + _status == WL_CONNECTED ? "true" : "false", + _ssid.c_str(), + _status == WL_CONNECTED ? _localIP.toString().c_str() : "", + _rssi); fflush(stdout); + } +public: + void mode(int) {} + int begin(const char* ssid, const char* pass) { + _ssid = ssid ? ssid : ""; + _pass = pass ? pass : ""; + _status = WL_DISCONNECTED; + _connSent = false; + printf("\\x02WIFI:{\\"event\\":\\"begin\\",\\"ssid\\":\\"%s\\"}\\n", _ssid.c_str()); fflush(stdout); + return WL_DISCONNECTED; + } + int begin(const char* ssid) { return begin(ssid, ""); } + int status() { + // Simulate async connection after first status poll following begin() + if (!_connSent && _ssid.length() > 0) { + _status = WL_CONNECTED; + _localIP = IPAddress(192,168,1,123); + _gw = IPAddress(192,168,1,1); + _mask = IPAddress(255,255,255,0); + _rssi = -42; + _connSent = true; + _emitState(); + } + return _status; + } + void disconnect(bool wifioff=false) { + _status = WL_DISCONNECTED; + _ssid = ""; + _pass = ""; + _localIP = IPAddress(0,0,0,0); + _rssi = -99; + _connSent = false; + printf("\\x02WIFI:{\\"event\\":\\"disconnect\\"}\\n"); fflush(stdout); + } + IPAddress localIP() { return _localIP; } + IPAddress gatewayIP() { return _gw; } + IPAddress subnetMask() { return _mask; } + String macAddress() { return String("AA:BB:CC:DD:EE:FF"); } + String SSID() { return _ssid; } + int RSSI() { return _rssi; } + bool softAP(const char* s, const char* p = nullptr, int c = 1, bool h = false) { (void)s;(void)p;(void)c;(void)h; return false; } + void softAPdisconnect(bool=false) {} + IPAddress softAPIP() { return {}; } + void hostname(const char*) {} + String hostname() { return String("esp8266"); } + bool setAutoReconnect(bool) { return false; } + bool setSleepMode(int) { return false; } + void printDiag(HardwareSerial&) {} +}; +extern ESP8266WiFiClass WiFi; + +// ── HTTPClient stub ─────────────────────────────────────────────────────────── +class HTTPClient { +public: + void begin(const char*) {} + void begin(const String&) {} + void begin(WiFiClient&, const char*) {} + void begin(WiFiClient&, const String&) {} + void end() {} + void addHeader(const String&, const String&) {} + void setTimeout(uint16_t) {} + int GET() { return -1; } + int POST(const String&) { return -1; } + int PUT(const String&) { return -1; } + int PATCH(const String&) { return -1; } + int DELETE_() { return -1; } + int sendRequest(const char* m, const String& b = String()) { (void)m;(void)b; return -1; } + String getString() { return String(); } + int getSize() { return -1; } + bool connected() { return false; } +}; + +#endif // _ESP8266_EMU_BRIDGE +`; + +// DHT mock for ESP8266 native (same protocol, uses _emb8266 namespace + printf) +const _DHT_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_DHT_MOCK_ESP8266_H +#define _EMU_DHT_MOCK_ESP8266_H +#include "gpio_bridge_esp8266.h" +#define DHT11 11 +#define DHT22 22 +#define DHT21 21 +#define AM2301 21 +class DHT { + uint8_t _pin; +public: + DHT(uint8_t pin, uint8_t type=DHT22, uint8_t count=6) : _pin(pin) {} + void begin(uint8_t usec=55) { + _emb8266::_dht_init_defaults(); + pinMode(_pin, INPUT); + printf("\\x02" "DINI:%d\\n", (int)_pin); fflush(stdout); + } + float readTemperature(bool S=false, bool force=false) { + _emb8266::_dht_poll(); + printf("\\x02G%d:1\\n", (int)_pin); fflush(stdout); + float t = _emb8266::_dht_t[_pin<20?_pin:0]; + printf("\\x02G%d:0\\n", (int)_pin); fflush(stdout); + return S ? t*9.0f/5.0f+32.0f : t; + } + float readHumidity(bool force=false) { + _emb8266::_dht_poll(); + printf("\\x02G%d:1\\n", (int)_pin); fflush(stdout); + float h = _emb8266::_dht_h[_pin<20?_pin:0]; + printf("\\x02G%d:0\\n", (int)_pin); fflush(stdout); + return h; + } + float computeHeatIndex(float temperature, float percentHumidity, bool isFahrenheit=true) { + // Adafruit DHT 라이브러리와 동일한 Rothfusz 회귀식 (입력을 화씨로 환산해 계산) + float t = isFahrenheit ? temperature : (temperature * 1.8f + 32.0f); + float hi = 0.5f * (t + 61.0f + ((t - 68.0f) * 1.2f) + (percentHumidity * 0.094f)); + if (hi > 79.0f) { + hi = -42.379f + 2.04901523f * t + 10.14333127f * percentHumidity + + -0.22475541f * t * percentHumidity + -0.00683783f * t * t + + -0.05481717f * percentHumidity * percentHumidity + + 0.00122874f * t * t * percentHumidity + + 0.00085282f * t * percentHumidity * percentHumidity + + -0.00000199f * t * t * percentHumidity * percentHumidity; + if ((percentHumidity < 13.0f) && (t >= 80.0f) && (t <= 112.0f)) + hi -= ((13.0f - percentHumidity) * 0.25f) * sqrtf((17.0f - ((t-95.0f)<0?-(t-95.0f):(t-95.0f))) * 0.05882f); + else if ((percentHumidity > 85.0f) && (t >= 80.0f) && (t <= 87.0f)) + hi += ((percentHumidity - 85.0f) * 0.1f) * ((87.0f - t) * 0.2f); + } + return isFahrenheit ? hi : (hi - 32.0f) * 0.55555f; + } + bool read(bool force=false) { return true; } + static bool isnan(float v) { return v!=v; } +}; +#endif +`; + +// Servo mock for ESP8266 native +const _SERVO_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_SERVO_MOCK_ESP8266_H +#define _EMU_SERVO_MOCK_ESP8266_H +#include "gpio_bridge_esp8266.h" +class Servo { + int _pin = -1; + int _angle = 90; +public: + bool attach(int pin, int minUs=544, int maxUs=2400) { + _pin = pin; + printf("\\x02" "SRVA:%d\\n", _pin); fflush(stdout); + return true; + } + void detach() { _pin = -1; } + void write(int angle) { + _angle = angle < 0 ? 0 : angle > 180 ? 180 : angle; + if (_pin >= 0) { printf("\\x02" "SRVO:%d:%d\\n", _pin, _angle); fflush(stdout); } + } + void writeMicroseconds(int us) { + int a = (int)((us - 544) * 180.0f / 1856.0f); + write(a < 0 ? 0 : a > 180 ? 180 : a); + } + int read() { return _angle; } + int readMicroseconds() { return 544 + (int)(_angle * 1856.0f / 180.0f); } + bool attached() { return _pin >= 0; } +}; +#endif +`; + +// Wire (I2C) mock for ESP8266 native +const _WIRE_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_WIRE_MOCK_ESP8266_H +#define _EMU_WIRE_MOCK_ESP8266_H +#include "gpio_bridge_esp8266.h" +class TwoWire { + uint8_t _addr = 0; + uint8_t _reg = 0; + uint8_t _rx[32]; + uint8_t _rxLen = 0; + uint8_t _rxPos = 0; + int _sda = -1, _scl = -1; + bool _initSent = false; + int16_t _accelVal(float g) { + int v = (int)(g * 16384.0f); + if (v > 32767) v = 32767; if (v < -32768) v = -32768; + return (int16_t)v; + } + int16_t _gyroVal(float dps) { + int v = (int)(dps * 131.0f); + if (v > 32767) v = 32767; if (v < -32768) v = -32768; + return (int16_t)v; + } +public: + void begin() { _sda = 4; _scl = 5; } + void begin(uint8_t addr) { _sda = 4; _scl = 5; (void)addr; } + void begin(int sda, int scl) { _sda = sda; _scl = scl; } + void setClock(uint32_t) {} + void setClockStretchLimit(uint32_t) {} + void beginTransmission(uint8_t addr) { + _addr = addr; + _emb8266::_dht_poll(); + if ((addr == 0x68 || addr == 0x69) && !_initSent) { + printf("\\x02IMU:%d:%d:%d\\n", _sda, _scl, (int)addr); fflush(stdout); + _initSent = true; + } + if (addr == 0x76 || addr == 0x77) { + printf("\\x02BMU:%d:%d:%d\\n", _sda, _scl, (int)addr); fflush(stdout); + } + } + uint8_t endTransmission(bool=true) { return 0; } + uint8_t requestFrom(uint8_t addr, uint8_t qty, bool=true) { + _emb8266::_dht_poll(); + _rxPos = 0; _rxLen = 0; + if (qty > 32) qty = 32; + if (addr == 0x68 || addr == 0x69) { + if (_reg == 0x75) { _rx[0] = addr; _rxLen = 1; } + else if (_reg == 0x3B) { + pthread_mutex_lock(&_emb8266::_dht_mu); + float* v = _emb8266::_imu_values[addr]; + int16_t ax = _accelVal(v[0]), ay = _accelVal(v[1]), az = _accelVal(v[2]); + int16_t gx = _gyroVal(v[3]), gy = _gyroVal(v[4]), gz = _gyroVal(v[5]); + int16_t temp = (int16_t)((v[6] + 21.0f) * 340.0f); + pthread_mutex_unlock(&_emb8266::_dht_mu); + _rx[0] = ax >> 8; _rx[1] = ax & 0xFF; + _rx[2] = ay >> 8; _rx[3] = ay & 0xFF; + _rx[4] = az >> 8; _rx[5] = az & 0xFF; + _rx[6] = temp >> 8; _rx[7] = temp & 0xFF; + _rx[8] = gx >> 8; _rx[9] = gx & 0xFF; + _rx[10] = gy >> 8; _rx[11] = gy & 0xFF; + _rx[12] = gz >> 8; _rx[13] = gz & 0xFF; + _rxLen = 14; + } + } else if (addr == 0x76 || addr == 0x77) { + if (_reg == 0xD0) { _rx[0] = 0x60; _rxLen = 1; } + else if (_reg == 0xF7) { + pthread_mutex_lock(&_emb8266::_dht_mu); + float t = _emb8266::_bme_values[addr][0]; + float h = _emb8266::_bme_values[addr][1]; + float p = _emb8266::_bme_values[addr][2]; + pthread_mutex_unlock(&_emb8266::_dht_mu); + uint32_t pp = (uint32_t)((p * 100.0f) * 256.0f); + int32_t tt = (int32_t)((t + 40.0f) * 100.0f * 65536.0f / 165.0f); + uint32_t hh = (uint32_t)(h * 1024.0f / 100.0f); + _rx[0] = (pp >> 16) & 0xFF; _rx[1] = (pp >> 8) & 0xFF; _rx[2] = pp & 0xFF; + _rx[3] = (tt >> 16) & 0xFF; _rx[4] = (tt >> 8) & 0xFF; _rx[5] = tt & 0xFF; + _rx[6] = (hh >> 8) & 0xFF; _rx[7] = hh & 0xFF; + _rxLen = 8; + } + } + return _rxLen; + } + size_t write(uint8_t b) { _reg = b; return 1; } + size_t write(const uint8_t* d, size_t n) { if (n) _reg = d[0]; return n; } + int available() { return _rxLen - _rxPos; } + int read() { return _rxPos < _rxLen ? _rx[_rxPos++] : -1; } + int peek() { return _rxPos < _rxLen ? _rx[_rxPos] : -1; } + void flush() {} + void onReceive(void(*)(int)) {} + void onRequest(void(*)(void)) {} +}; +extern TwoWire Wire; +#endif +`; + +// MPU6050 mock for ESP8266 native +const _MPU6050_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_MPU6050_MOCK_ESP8266_H +#define _EMU_MPU6050_MOCK_ESP8266_H +#include "Wire.h" +class MPU6050 { + uint8_t _addr; +public: + MPU6050(uint8_t addr=0x68) : _addr(addr) {} + void initialize() { + Wire.begin(); + Wire.beginTransmission(_addr); + Wire.write(0x6B); + Wire.write(0); + Wire.endTransmission(); + Wire.beginTransmission(_addr); + Wire.requestFrom(_addr, (uint8_t)14); + } + bool testConnection() { return true; } + void getMotion6(int16_t* ax, int16_t* ay, int16_t* az, int16_t* gx, int16_t* gy, int16_t* gz) { + _emb8266::_dht_poll(); + Wire.beginTransmission(_addr); + Wire.write(0x3B); + Wire.endTransmission(); + Wire.requestFrom(_addr, (uint8_t)14); + *ax = (Wire.read() << 8) | Wire.read(); + *ay = (Wire.read() << 8) | Wire.read(); + *az = (Wire.read() << 8) | Wire.read(); + Wire.read(); Wire.read(); + *gx = (Wire.read() << 8) | Wire.read(); + *gy = (Wire.read() << 8) | Wire.read(); + *gz = (Wire.read() << 8) | Wire.read(); + } + int16_t getTemperature() { + _emb8266::_dht_poll(); + Wire.beginTransmission(_addr); + Wire.write(0x3B); + Wire.endTransmission(); + Wire.requestFrom(_addr, (uint8_t)8); + for (int i=0;i<6;i++) Wire.read(); + int16_t t = (Wire.read() << 8) | Wire.read(); + return t; + } +}; +#endif +`; + +// BME280 mock for ESP8266 native +const _BME280_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_BME280_MOCK_ESP8266_H +#define _EMU_BME280_MOCK_ESP8266_H +#include "Wire.h" +class Adafruit_BME280 { + uint8_t _addr; +public: + Adafruit_BME280() : _addr(0x76) {} + bool begin(uint8_t addr=0x76, TwoWire* wire=nullptr) { + _addr = addr; + (void)wire; + Wire.begin(); + Wire.beginTransmission(_addr); + Wire.write(0xD0); + Wire.endTransmission(false); + Wire.requestFrom(_addr, (uint8_t)1); + Wire.read(); + Wire.beginTransmission(_addr); + Wire.write(0xF4); + Wire.write(0x27); + Wire.endTransmission(); + return true; + } + float readTemperature() { + _emb8266::_dht_poll(); + pthread_mutex_lock(&_emb8266::_dht_mu); + float v = _emb8266::_bme_values[_addr][0]; + pthread_mutex_unlock(&_emb8266::_dht_mu); + return v; + } + float readPressure() { + _emb8266::_dht_poll(); + pthread_mutex_lock(&_emb8266::_dht_mu); + float v = _emb8266::_bme_values[_addr][2]; + pthread_mutex_unlock(&_emb8266::_dht_mu); + return v; + } + float readHumidity() { + _emb8266::_dht_poll(); + pthread_mutex_lock(&_emb8266::_dht_mu); + float v = _emb8266::_bme_values[_addr][1]; + pthread_mutex_unlock(&_emb8266::_dht_mu); + return v; + } + float readAltitude(float seaLevel=1013.25) { + float p = readPressure(); + return 44330.0f * (1.0f - powf(p / seaLevel, 0.1903f)); + } +}; +class BME280 : public Adafruit_BME280 {}; +#endif +`; + +// OneWire mock for ESP8266 native +const _ONEWIRE_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_ONEWIRE_MOCK_ESP8266_H +#define _EMU_ONEWIRE_MOCK_ESP8266_H +#include "gpio_bridge_esp8266.h" +class OneWire { + uint8_t _pin; + bool _initSent; +public: + OneWire(uint8_t pin) : _pin(pin), _initSent(false) { _emb8266::_ow_last_pin = pin; } + bool reset(void) { return true; } + void write(uint8_t v, uint8_t p=0) { (void)v; (void)p; } + uint8_t read() { return 0xBE; } + void reset_search() {} + bool search(uint8_t* addr, bool normal=true) { + _emb8266::_dht_poll(); + _emb8266::_ow_last_pin = _pin; + if (!_initSent) { + printf("\\x02OWI:%d\\n", (int)_pin); fflush(stdout); + _initSent = true; + } + uint8_t id[8] = {0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + memcpy(addr, id, 8); + return true; + } + void select(const uint8_t* addr) { (void)addr; } +}; +#endif +`; + +// DallasTemperature mock for ESP8266 native +const _DALLASTEMP_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_DALLASTEMP_MOCK_ESP8266_H +#define _EMU_DALLASTEMP_MOCK_ESP8266_H +#include "OneWire.h" +class DallasTemperature { + OneWire* _wire; +public: + DallasTemperature(OneWire* wire) : _wire(wire) {} + void begin() {} + void setResolution(uint8_t r) { (void)r; } + void requestTemperatures() {} + float getTempCByIndex(uint8_t idx) { + _emb8266::_dht_poll(); + pthread_mutex_lock(&_emb8266::_dht_mu); + int pin = _emb8266::_ow_last_pin; + float v = _emb8266::_ow_t[pin >= 0 && pin < 20 ? pin : 0]; + pthread_mutex_unlock(&_emb8266::_dht_mu); + return v; + } + float getTempFByIndex(uint8_t idx) { + float c = getTempCByIndex(idx); + return c * 9.0f / 5.0f + 32.0f; + } +}; +#endif +`; + +// Relay module mock for ESP8266 native +const _RELAY_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_RELAY_MOCK_ESP8266_H +#define _EMU_RELAY_MOCK_ESP8266_H +#include "gpio_bridge_esp8266.h" +class Relay { + int _pin; + bool _on; +public: + Relay(int pin, bool activeLow=false) : _pin(pin), _on(false) { (void)activeLow; } + void begin() { + _emb8266::mode(_pin, 'O'); + printf("\\x02RLY:%d:0\\n", _pin); fflush(stdout); + } + void on() { _on = true; _emb8266::pin(_pin, 1); printf("\\x02RLY:%d:1\\n", _pin); fflush(stdout); } + void off() { _on = false; _emb8266::pin(_pin, 0); printf("\\x02RLY:%d:0\\n", _pin); fflush(stdout); } + bool isOn() { return _on; } +}; +#endif +`; + +// L298N motor driver mock for ESP8266 native +const _L298N_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_L298N_MOCK_ESP8266_H +#define _EMU_L298N_MOCK_ESP8266_H +#include "gpio_bridge_esp8266.h" +class L298N { + int _enA, _in1, _in2, _enB, _in3, _in4; + int _speedA, _speedB; + bool _hasB; + void _emit() { + printf("\\x02MST:%d:%d:%d:%d:%d:%d\\n", _enA, _in1, _in2, _in3, _in4, _enB); fflush(stdout); + } +public: + L298N(int enA, int in1, int in2, int enB=0, int in3=0, int in4=0) + : _enA(enA), _in1(in1), _in2(in2), _enB(enB), _in3(in3), _in4(in4), _speedA(0), _speedB(0), _hasB(enB>0) { + printf("\\x02MOT:%d:%d:%d:%d:%d:%d\\n", enA, in1, in2, in3, in4, enB); fflush(stdout); + } + void begin() { + _emb8266::mode(_enA, 'O'); _emb8266::mode(_in1, 'O'); _emb8266::mode(_in2, 'O'); + if (_hasB) { _emb8266::mode(_enB, 'O'); _emb8266::mode(_in3, 'O'); _emb8266::mode(_in4, 'O'); } + _emit(); + } + void setSpeedA(int s) { _speedA = s; _emb8266::pin(_enA, s > 0 ? 1 : 0); _emit(); } + void setSpeedB(int s) { _speedB = s; if (_hasB) _emb8266::pin(_enB, s > 0 ? 1 : 0); _emit(); } + void forwardA() { _emb8266::pin(_in1, 1); _emb8266::pin(_in2, 0); _emit(); } + void backwardA() { _emb8266::pin(_in1, 0); _emb8266::pin(_in2, 1); _emit(); } + void stopA() { _emb8266::pin(_in1, 0); _emb8266::pin(_in2, 0); _emit(); } + void forwardB() { if (_hasB) { _emb8266::pin(_in3, 1); _emb8266::pin(_in4, 0); _emit(); } } + void backwardB() { if (_hasB) { _emb8266::pin(_in3, 0); _emb8266::pin(_in4, 1); _emit(); } } + void stopB() { if (_hasB) { _emb8266::pin(_in3, 0); _emb8266::pin(_in4, 0); _emit(); } } + void forward() { forwardA(); if (_hasB) forwardB(); } + void backward() { backwardA(); if (_hasB) backwardB(); } + void stop() { stopA(); if (_hasB) stopB(); } +}; +#endif +`; + +// WiFi stub headers +const _ESP8266_WIFI_H = `#pragma once +#include "gpio_bridge_esp8266.h" +`; +const _ESP8266_HTTP_H = `#pragma once +#include "gpio_bridge_esp8266.h" +`; + +// ESP8266 main entry point — compiled as a separate .cpp to avoid multiple-definition of main +const _ESP8266_MAIN_CPP = `#include "gpio_bridge_esp8266.h" +HardwareSerial Serial; +HardwareSerial Serial1; +ESPClass ESP; +EEPROMClass EEPROM; +TwoWire Wire; +SPIClass SPI; +FSClass LittleFS; +FSClass SPIFFS; +ESP8266WiFiClass WiFi; +// sketch.ino is included here so all code shares one translation unit. +// This prevents duplicated static variables in _emb8266 namespace. +#include "sketch.ino" +int main() { + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + _emu_init_time(); + setup(); + while (true) { loop(); } + return 0; +} +`; + +// Mock TFT library (ESP8266 native) — printf/fflush instead of Serial.printf; includes minimal Print base +const _TFT_MOCK_ESP8266_H = `#pragma once +#ifndef _EMU_TFT_MOCK_ESP8266_H +#define _EMU_TFT_MOCK_ESP8266_H +#define TFT_BLACK 0x0000 +#define TFT_WHITE 0xFFFF +#define TFT_RED 0xF800 +#define TFT_GREEN 0x07E0 +#define TFT_BLUE 0x001F +#define TFT_CYAN 0x07FF +#define TFT_MAGENTA 0xF81F +#define TFT_YELLOW 0xFFE0 +#define TFT_ORANGE 0xFD20 +#define TFT_DARKGREY 0x7BEF +#define TFT_LIGHTGREY 0xC618 +#define TFT_NAVY 0x000F +#define TFT_DARKGREEN 0x03E0 +#define TFT_DARKCYAN 0x03EF +#define TFT_MAROON 0x7800 +#define TFT_PURPLE 0x780F +#define TFT_OLIVE 0x7BE0 +#define TFT_GREENYELLOW 0xB7E0 +#define TFT_PINK 0xFC18 +#define ILI9341_BLACK TFT_BLACK +#define ILI9341_WHITE TFT_WHITE +#define ILI9341_RED TFT_RED +#define ILI9341_GREEN TFT_GREEN +#define ILI9341_BLUE TFT_BLUE +#define ILI9341_CYAN TFT_CYAN +#define ILI9341_MAGENTA TFT_MAGENTA +#define ILI9341_YELLOW TFT_YELLOW +#define ILI9341_ORANGE TFT_ORANGE +#define ST77XX_BLACK TFT_BLACK +#define ST77XX_WHITE TFT_WHITE +#define ST77XX_RED TFT_RED +#define ST77XX_GREEN TFT_GREEN +#define ST77XX_BLUE TFT_BLUE +#define ST77XX_CYAN TFT_CYAN +#define ST77XX_MAGENTA TFT_MAGENTA +#define ST77XX_YELLOW TFT_YELLOW +#define TL_DATUM 0 +#define TC_DATUM 1 +#define TR_DATUM 2 +namespace _emb_tft { + static int16_t W=240, H=320, cx=0, cy=0; + static uint16_t fg=0xFFFF, bg=0x0000; + static uint8_t ts=1; + static inline void feedWdt(){} +} +class Print { +public: + virtual size_t write(uint8_t) = 0; + size_t write(const uint8_t* b, size_t n){ size_t t=0; for(size_t i=0;i size_t println(T v){ size_t n=print(v); return n+write('\\n'); } + template size_t println(T v,A a){ size_t n=print(v,a); return n+write('\\n'); } +}; +class TFT_eSPI : public Print { + int16_t _w, _h; +public: + int8_t _cs, _dc; + TFT_eSPI(int16_t w=240, int16_t h=320, int8_t cs=-1, int8_t dc=-1) : _w(w), _h(h), _cs(cs), _dc(dc) {} + void begin() { init(); } + void init() { + _emb_tft::W=_w; _emb_tft::H=_h; + printf("\\x02TI:%d,%d\\n",(int)_w,(int)_h); + // SPI 핀 OUTPUT 설정: SCK=14(D5), MOSI=13(D7), MISO=12(D6 INPUT) + printf("\\x02M14:O\\n\\x02G14:0\\n\\x02M13:O\\n\\x02G13:0\\n\\x02M12:I\\n"); + if(_cs>=0) printf("\\x02M%d:O\\n\\x02G%d:1\\n",(int)_cs,(int)_cs); + if(_dc>=0) printf("\\x02M%d:O\\n\\x02G%d:0\\n",(int)_dc,(int)_dc); + fflush(stdout); + } + int16_t width() { return _w; } + int16_t height() { return _h; } + void setRotation(uint8_t r) { + if((r==1||r==3)&&_w!=_h){int16_t t=_w;_w=_h;_h=t;_emb_tft::W=_w;_emb_tft::H=_h;} + printf("\\x02TR:%d\\n",(int)r); fflush(stdout); + } + void fillScreen(uint32_t c) { printf("\\x02TF:%d\\n",(int)(c&0xFFFF)); fflush(stdout); } + void fillRect(int32_t x,int32_t y,int32_t w,int32_t h,uint32_t c) + { printf("\\x02TB:%d,%d,%d,%d,%d\\n",(int)x,(int)y,(int)w,(int)h,(int)(c&0xFFFF)); fflush(stdout); } + void drawRect(int32_t x,int32_t y,int32_t w,int32_t h,uint32_t c) + { drawFastHLine(x,y,w,c);drawFastHLine(x,y+h-1,w,c);drawFastVLine(x,y,h,c);drawFastVLine(x+w-1,y,h,c); } + void drawPixel(int32_t x,int32_t y,uint32_t c) + { printf("\\x02TP:%d,%d,%d\\n",(int)x,(int)y,(int)(c&0xFFFF)); fflush(stdout); } + void drawFastHLine(int32_t x,int32_t y,int32_t w,uint32_t c){ fillRect(x,y,w,1,c); } + void drawFastVLine(int32_t x,int32_t y,int32_t h,uint32_t c){ fillRect(x,y,1,h,c); } + void drawLine(int32_t x0,int32_t y0,int32_t x1,int32_t y1,uint32_t c) + { printf("\\x02TL:%d,%d,%d,%d,%d\\n",(int)x0,(int)y0,(int)x1,(int)y1,(int)(c&0xFFFF)); fflush(stdout); } + void drawCircle(int32_t x,int32_t y,int32_t r,uint32_t c) + { printf("\\x02TC:%d,%d,%d,%d,0\\n",(int)x,(int)y,(int)r,(int)(c&0xFFFF)); fflush(stdout); } + void fillCircle(int32_t x,int32_t y,int32_t r,uint32_t c) + { printf("\\x02TC:%d,%d,%d,%d,1\\n",(int)x,(int)y,(int)r,(int)(c&0xFFFF)); fflush(stdout); } + void fillRoundRect(int32_t x,int32_t y,int32_t w,int32_t h,int32_t r,uint32_t c){ fillRect(x,y,w,h,c); } + void drawRoundRect(int32_t x,int32_t y,int32_t w,int32_t h,int32_t r,uint32_t c){ drawRect(x,y,w,h,c); } + void fillTriangle(int32_t x0,int32_t y0,int32_t x1,int32_t y1,int32_t x2,int32_t y2,uint32_t c) + { drawLine(x0,y0,x1,y1,c);drawLine(x1,y1,x2,y2,c);drawLine(x2,y2,x0,y0,c); } + void setTextColor(uint32_t c){ _emb_tft::fg=(uint16_t)(c&0xFFFF); } + void setTextColor(uint32_t f,uint32_t b){ _emb_tft::fg=(uint16_t)(f&0xFFFF);_emb_tft::bg=(uint16_t)(b&0xFFFF); } + void setTextSize(uint8_t s){ _emb_tft::ts=s; } + void setCursor(int16_t x,int16_t y){ _emb_tft::cx=x;_emb_tft::cy=y; } + void setTextDatum(uint8_t d){} + void setTextPadding(uint16_t x){} + void setTextFont(uint8_t f){} + int16_t getCursorX(){ return _emb_tft::cx; } + int16_t getCursorY(){ return _emb_tft::cy; } + size_t write(uint8_t c) override { + if(c=='\\n'){ _emb_tft::cy+=8*_emb_tft::ts; _emb_tft::cx=0; return 1; } + if(c=='\\r') return 1; + printf("\\x02TT:%d,%d,%d,%d,%d,%d\\n", + (int)_emb_tft::cx,(int)_emb_tft::cy,(int)_emb_tft::fg,(int)_emb_tft::bg,(int)_emb_tft::ts,(int)c); + fflush(stdout); + _emb_tft::cx+=6*_emb_tft::ts; + return 1; + } + void drawString(const char* s,int32_t x,int32_t y,uint8_t f=2){ setCursor(x,y);print(s); } + void drawString(const String& s,int32_t x,int32_t y,uint8_t f=2){ drawString(s.c_str(),x,y,f); } + void drawCentreString(const char* s,int32_t x,int32_t y,uint8_t f=2){ setCursor(x-(int16_t)(strlen(s)*3*_emb_tft::ts),y);print(s); } + void drawNumber(long n,int32_t x,int32_t y,uint8_t f=2){ setCursor(x,y);print(n); } + void drawFloat(float n,uint8_t dp,int32_t x,int32_t y,uint8_t f=2){ setCursor(x,y);print(n,dp); } + int16_t textWidth(const char* s, uint8_t f=2){ return (int16_t)(strlen(s)*6*_emb_tft::ts); } + int16_t textWidth(const String& s, uint8_t f=2){ return textWidth(s.c_str(),f); } + static uint16_t color565(uint8_t r,uint8_t g,uint8_t b){ return ((r&0xF8)<<8)|((g&0xFC)<<3)|(b>>3); } + static uint16_t color24to16(uint32_t c){ return color565((c>>16)&0xFF,(c>>8)&0xFF,c&0xFF); } +}; +class Adafruit_ILI9341 : public TFT_eSPI { +public: + Adafruit_ILI9341(int8_t cs=-1,int8_t dc=-1,int8_t mosi=-1,int8_t clk=-1,int8_t rst=-1,int8_t miso=-1) + : TFT_eSPI(240,320,cs,dc){} + void startWrite(){} + void endWrite(){} +}; +class Adafruit_ST7789 : public TFT_eSPI { +public: + Adafruit_ST7789(int8_t cs=-1,int8_t dc=-1,int8_t rst=-1): TFT_eSPI(240,320,cs,dc){} +}; +class Adafruit_ST7735 : public TFT_eSPI { +public: + Adafruit_ST7735(int8_t cs=-1,int8_t dc=-1,int8_t rst=-1): TFT_eSPI(128,160,cs,dc){} + void initR(uint8_t opts=0){ init(); } + void initB(){ init(); } + void initG(){ init(); } +}; +#endif +`; + +// ── ESP32 QEMU emulator sessions ───────────────────────────────────────────── +const _qemuPath = require('os').homedir() + '/.local/lib/esp-qemu/bin/qemu-system-xtensa'; +interface QemuSession { + pty: any | null; tmpDir: string; fqbn: string; + listeners: Set<(d: string) => void>; + sseConns: Set; + timer: ReturnType; + ptyBuf: string; // buffer for parsing GPIO bridge protocol + gpioBuf: string[]; // buffered GPIO/DHT/TFT events for late SSE connections +} +const qemuSessions = new Map(); + +// gpio_bridge.h content — auto-injected into every ESP32 sketch +const _GPIO_BRIDGE_H = `#pragma once +#ifndef _ESP32_EMU_BRIDGE +#define _ESP32_EMU_BRIDGE +#ifdef __cplusplus +#include +namespace _emb { + static inline void pin(int p, int v) { + if (Serial) Serial.printf("\\x02G%d:%d\\n", p, v); + } + static inline void mode(int p, char m) { + if (Serial) Serial.printf("\\x02M%d:%c\\n", p, m); + } + // DHT mock globals — updated by host via \\x02DSET:::\\n + static float _dht_t[40]; + static float _dht_h[40]; + static bool _dht_init_done = false; + // I2C sensor globals: index = I2C address (0-127), values updated via ISET/BSET + static float _imu_values[128][7]; // per addr: ax,ay,az,gx,gy,gz,temp + static bool _imu_init_done = false; + static float _bme_values[128][3]; // per addr: temp, hum, pressure + static bool _bme_init_done = false; + // OneWire globals: index = GPIO pin + static float _ow_t[40]; + static bool _ow_init_done = false; + static int _ow_last_pin = 0; + // Motor globals: ENA,IN1,IN2,IN3,IN4,ENB + static int _motor_values[6] = {-1,-1,-1,-1,-1,-1}; + static String _dht_cmd_buf; + static bool _dht_in_cmd = false; + static void _dht_init_defaults() { + if (_dht_init_done) return; + _dht_init_done = true; + for (int i=0;i<40;i++){_dht_t[i]=25.0f;_dht_h[i]=50.0f;} + } + static void _sensor_init_defaults() { + if (!_imu_init_done) { + _imu_init_done = true; + for (int a=0;a<128;a++) for (int v=0;v<7;v++) _imu_values[a][v]=0; + for (int a=0;a<128;a++) { _imu_values[a][2]=1.0f; _imu_values[a][6]=25.0f; } + } + if (!_bme_init_done) { + _bme_init_done = true; + for (int a=0;a<128;a++) { _bme_values[a][0]=25.0f; _bme_values[a][1]=50.0f; _bme_values[a][2]=1013.0f; } + } + if (!_ow_init_done) { + _ow_init_done = true; + for (int i=0;i<40;i++) _ow_t[i]=25.0f; + } + } + // Parse host commands: DSET, ISET, BSET, OSET, MSET + static void _parse_cmd(const String& cmd) { + if (cmd.startsWith("DSET:")) { + int p1=cmd.indexOf(':'), p2=cmd.indexOf(':',p1+1), p3=cmd.indexOf(':',p2+1); + if (p2>p1 && p3>p2) { + int pin=cmd.substring(p1+1,p2).toInt(); + float t=cmd.substring(p2+1,p3).toFloat(), h=cmd.substring(p3+1).toFloat(); + if (pin>=0&&pin<40){_dht_t[pin]=t;_dht_h[pin]=h;} + } + } else if (cmd.startsWith("ISET:")) { + // ISET:addr:ax:ay:az:gx:gy:gz:temp + int p[9]; p[0]=cmd.indexOf(':'); + for (int i=1;i<8;i++) p[i]=cmd.indexOf(':',p[i-1]+1); + int addr=cmd.substring(p[0]+1,p[1]).toInt(); + if (addr>=0 && addr<128) { + for (int i=0;i<7;i++) _imu_values[addr][i]=cmd.substring(p[i+1]+1,p[i+2]).toFloat(); + } + } else if (cmd.startsWith("BSET:")) { + // BSET:addr:t:h:p + int p1=cmd.indexOf(':'), p2=cmd.indexOf(':',p1+1), p3=cmd.indexOf(':',p2+1), p4=cmd.indexOf(':',p3+1); + int addr=cmd.substring(p1+1,p2).toInt(); + if (addr>=0 && addr<128) { + _bme_values[addr][0]=cmd.substring(p2+1,p3).toFloat(); + _bme_values[addr][1]=cmd.substring(p3+1,p4).toFloat(); + _bme_values[addr][2]=cmd.substring(p4+1).toFloat(); + } + } else if (cmd.startsWith("OSET:")) { + // OSET:pin:temp + int p1=cmd.indexOf(':'), p2=cmd.indexOf(':',p1+1); + int pin=cmd.substring(p1+1,p2).toInt(); + float t=cmd.substring(p2+1).toFloat(); + if (pin>=0&&pin<40) _ow_t[pin]=t; + } else if (cmd.startsWith("MSET:")) { + // MSET:enA:in1:in2:in3:in4:enB + int p[7]; p[0]=cmd.indexOf(':'); + for (int i=1;i<6;i++) p[i]=cmd.indexOf(':',p[i-1]+1); + for (int i=0;i<6;i++) _motor_values[i]=cmd.substring(p[i]+1,p[i+1]).toInt(); + } + } + // Only consumes \\x02-prefixed commands; non-bridge bytes stay in Serial buffer + static void _dht_poll() { + _sensor_init_defaults(); + for (;;) { + if (!_dht_in_cmd) { + if (!Serial.available() || Serial.peek() != 0x02) return; + } else { + if (!Serial.available()) return; + } + char c = (char)Serial.read(); + if (!_dht_in_cmd) { _dht_in_cmd=true; _dht_cmd_buf=""; } + else if (c == '\\n') { + _parse_cmd(_dht_cmd_buf); + _dht_in_cmd=false; + } else _dht_cmd_buf+=c; + } + } +} +#ifndef _EM_NO_OVERRIDE +#define digitalWrite(p, v) do { ::digitalWrite(p,v); _emb::pin((int)(p),(int)(v)); } while(0) +#define pinMode(p, m) do { ::pinMode(p,m); _emb::mode((int)(p),(m)==OUTPUT?'O':'I'); } while(0) +// delay() override: use inline function to avoid macro/ODR issues with Arduino SDK +static inline void _em_delay(unsigned long ms) { _emb::_dht_poll(); if(ms>0) vTaskDelay(pdMS_TO_TICKS(ms)); } +#define delay(ms) _em_delay(ms) +#endif +#endif +#endif +`; + +// Mock DHT library — injected when sketch includes +const _DHT_MOCK_H = `#pragma once +#ifndef _EMU_DHT_MOCK_H +#define _EMU_DHT_MOCK_H +#include "gpio_bridge.h" +#define DHT11 11 +#define DHT22 22 +#define DHT21 21 +#define AM2301 21 +class DHT { + uint8_t _pin; +public: + DHT(uint8_t pin, uint8_t type=DHT22, uint8_t count=6) : _pin(pin) {} + void begin(uint8_t usec=55) { + _emb::_dht_init_defaults(); + pinMode(_pin, INPUT); + Serial.printf("\\x02" "DINI:%d\\n", (int)_pin); + } + float readTemperature(bool S=false, bool force=false) { + _emb::_dht_poll(); + Serial.printf("\\x02" "G%d:1\\n", (int)_pin); + float t = _emb::_dht_t[_pin<40?_pin:0]; + Serial.printf("\\x02" "G%d:0\\n", (int)_pin); + return S ? t*9.0f/5.0f+32.0f : t; + } + float readHumidity(bool force=false) { + _emb::_dht_poll(); + Serial.printf("\\x02" "G%d:1\\n", (int)_pin); + float h = _emb::_dht_h[_pin<40?_pin:0]; + Serial.printf("\\x02" "G%d:0\\n", (int)_pin); + return h; + } + float computeHeatIndex(float temperature, float percentHumidity, bool isFahrenheit=true) { + // Adafruit DHT 라이브러리와 동일한 Rothfusz 회귀식 (입력을 화씨로 환산해 계산) + float t = isFahrenheit ? temperature : (temperature * 1.8f + 32.0f); + float hi = 0.5f * (t + 61.0f + ((t - 68.0f) * 1.2f) + (percentHumidity * 0.094f)); + if (hi > 79.0f) { + hi = -42.379f + 2.04901523f * t + 10.14333127f * percentHumidity + + -0.22475541f * t * percentHumidity + -0.00683783f * t * t + + -0.05481717f * percentHumidity * percentHumidity + + 0.00122874f * t * t * percentHumidity + + 0.00085282f * t * percentHumidity * percentHumidity + + -0.00000199f * t * t * percentHumidity * percentHumidity; + if ((percentHumidity < 13.0f) && (t >= 80.0f) && (t <= 112.0f)) + hi -= ((13.0f - percentHumidity) * 0.25f) * sqrtf((17.0f - ((t-95.0f)<0?-(t-95.0f):(t-95.0f))) * 0.05882f); + else if ((percentHumidity > 85.0f) && (t >= 80.0f) && (t <= 87.0f)) + hi += ((percentHumidity - 85.0f) * 0.1f) * ((87.0f - t) * 0.2f); + } + return isFahrenheit ? hi : (hi - 32.0f) * 0.55555f; + } + bool read(bool force=false) { return true; } + static bool isnan(float v) { return v!=v; } +}; +#endif +`; + +// Servo mock for ESP32 QEMU (uses Serial.printf like DHT mock) +const _SERVO_MOCK_H = `#pragma once +#ifndef _EMU_SERVO_MOCK_H +#define _EMU_SERVO_MOCK_H +class Servo { + int _pin = -1; + int _angle = 90; +public: + bool attach(int pin, int minUs=544, int maxUs=2400) { + _pin = pin; + Serial.printf("\\x02" "SRVA:%d\\n", _pin); + return true; + } + void detach() { _pin = -1; } + void write(int angle) { + _angle = angle < 0 ? 0 : angle > 180 ? 180 : angle; + if (_pin >= 0) Serial.printf("\\x02" "SRVO:%d:%d\\n", _pin, _angle); + } + void writeMicroseconds(int us) { + int a = (int)((us - 544) * 180.0f / 1856.0f); + write(a < 0 ? 0 : a > 180 ? 180 : a); + } + int read() { return _angle; } + int readMicroseconds() { return 544 + (int)(_angle * 1856.0f / 180.0f); } + bool attached() { return _pin >= 0; } +}; +typedef Servo ESP32Servo; +#endif +`; + +// Wire (I2C) mock for ESP32 QEMU — sensor-aware, emits init events and serves register reads +// WiFi mock for ESP32 QEMU +const _WIFI_MOCK_H = `#pragma once +#ifndef _EMU_WIFI_MOCK_H +#define _EMU_WIFI_MOCK_H +#include "gpio_bridge.h" +#define WIFI_STA 1 +#define WIFI_AP 2 +#define WIFI_AP_STA 3 +#define WL_CONNECTED 3 +#define WL_DISCONNECTED 6 +#define WL_CONNECT_FAILED 4 +#define WL_NO_SHIELD 255 +class IPAddress { + uint8_t _d[4]; +public: + IPAddress(uint8_t a=0,uint8_t b=0,uint8_t c=0,uint8_t d=0){_d[0]=a;_d[1]=b;_d[2]=c;_d[3]=d;} + IPAddress(uint32_t v){_d[0]=(v)&0xFF;_d[1]=(v>>8)&0xFF;_d[2]=(v>>16)&0xFF;_d[3]=(v>>24)&0xFF;} + String toString() const { + char buf[20]; snprintf(buf,20,"%d.%d.%d.%d",_d[0],_d[1],_d[2],_d[3]); return String(buf); + } + bool operator==(const IPAddress& o) const { return memcmp(_d,o._d,4)==0; } + uint8_t operator[](int i) const { return _d[i]; } +}; +class WiFiClient { +public: + bool connect(const char*, uint16_t) { return false; } + bool connect(IPAddress, uint16_t) { return false; } + bool connected() { return false; } + void stop() {} + int available() { return 0; } + int read() { return -1; } + int read(uint8_t*, size_t) { return 0; } + size_t write(uint8_t) { return 0; } + size_t write(const uint8_t*, size_t n) { return n; } + void flush() {} + void setTimeout(int) {} + String readStringUntil(char) { return String(); } + operator bool() { return false; } +}; +class WiFiServer { +public: + WiFiServer(uint16_t) {} + void begin() {} + WiFiClient available() { return {}; } +}; +class WiFiClass { + String _ssid; + String _pass; + int _status = WL_DISCONNECTED; + IPAddress _localIP; + IPAddress _gw; + IPAddress _mask; + int _rssi = -99; + bool _connSent = false; + void _emitState() { + Serial.printf("\\x02WIFI:{\\\"connected\\\":%s,\\\"ssid\\\":\\\"%s\\\",\\\"ip\\\":\\\"%s\\\",\\\"rssi\\\":%d}\\n", + _status == WL_CONNECTED ? "true" : "false", + _ssid.c_str(), + _status == WL_CONNECTED ? _localIP.toString().c_str() : "", + _rssi); + } +public: + void mode(int) {} + int begin(const char* ssid, const char* pass = nullptr) { + _ssid = ssid ? ssid : ""; + _pass = pass ? pass : ""; + _status = WL_DISCONNECTED; + _connSent = false; + Serial.printf("\\x02WIFI:{\\\"event\\\":\\\"begin\\\",\\\"ssid\\\":\\\"%s\\\"}\\n", _ssid.c_str()); + return WL_DISCONNECTED; + } + int status() { + if (!_connSent && _ssid.length() > 0) { + _status = WL_CONNECTED; + _localIP = IPAddress(192,168,1,123); + _gw = IPAddress(192,168,1,1); + _mask = IPAddress(255,255,255,0); + _rssi = -42; + _connSent = true; + _emitState(); + } + return _status; + } + void disconnect(bool wifioff=false) { + _status = WL_DISCONNECTED; + _ssid = ""; + _pass = ""; + _localIP = IPAddress(0,0,0,0); + _rssi = -99; + _connSent = false; + Serial.printf("\\x02WIFI:{\\\"event\\\":\\\"disconnect\\\"}\\n"); + } + IPAddress localIP() { return _localIP; } + IPAddress gatewayIP() { return _gw; } + IPAddress subnetMask() { return _mask; } + String macAddress() { return String("AA:BB:CC:DD:EE:FF"); } + String SSID() { return _ssid; } + int RSSI() { return _rssi; } + bool softAP(const char* s, const char* p = nullptr, int c = 1, bool h = false) { (void)s;(void)p;(void)c;(void)h; return false; } + void softAPdisconnect(bool=false) {} + IPAddress softAPIP() { return {}; } + void hostname(const char*) {} + String hostname() { return String("esp32"); } + bool setAutoReconnect(bool) { return false; } + bool setSleepMode(int) { return false; } + void printDiag(HardwareSerial&) {} +}; +extern WiFiClass WiFi; +class HTTPClient { +public: + void begin(const char*) {} + void begin(const String&) {} + void begin(WiFiClient&, const char*) {} + void begin(WiFiClient&, const String&) {} + void end() {} + void addHeader(const String&, const String&) {} + void setTimeout(uint16_t) {} + int GET() { return -1; } + int POST(const String&) { return -1; } + int PUT(const String&) { return -1; } + int PATCH(const String&) { return -1; } + int DELETE_() { return -1; } + int sendRequest(const char* m, const String& b = String()) { (void)m;(void)b; return -1; } + String getString() { return String(); } + int getSize() { return -1; } + bool connected() { return false; } +}; +#endif +`; + +const _WIRE_MOCK_H = `#pragma once +#ifndef _EMU_WIRE_MOCK_H +#define _EMU_WIRE_MOCK_H +#include "gpio_bridge.h" +class TwoWire { + uint8_t _addr = 0; + uint8_t _reg = 0; + uint8_t _rx[32]; + uint8_t _rxLen = 0; + uint8_t _rxPos = 0; + int _sda = -1, _scl = -1; + bool _initSent = false; + int16_t _accelVal(float g) { + int v = (int)(g * 16384.0f); + if (v > 32767) v = 32767; if (v < -32768) v = -32768; + return (int16_t)v; + } + int16_t _gyroVal(float dps) { + int v = (int)(dps * 131.0f); + if (v > 32767) v = 32767; if (v < -32768) v = -32768; + return (int16_t)v; + } +public: + void begin() { _sda = 21; _scl = 22; } + void begin(uint8_t addr) { _sda = 21; _scl = 22; (void)addr; } + void begin(int sda, int scl) { _sda = sda; _scl = scl; } + void setClock(uint32_t) {} + void setClockStretchLimit(uint32_t) {} + void beginTransmission(uint8_t addr) { + _addr = addr; + _emb::_dht_poll(); + if ((addr == 0x68 || addr == 0x69) && !_initSent) { + Serial.printf("\\x02IMU:%d:%d:%d\\n", _sda, _scl, (int)addr); + _initSent = true; + } + if (addr == 0x76 || addr == 0x77) { + Serial.printf("\\x02BMU:%d:%d:%d\\n", _sda, _scl, (int)addr); + } + } + uint8_t endTransmission(bool=true) { return 0; } + uint8_t requestFrom(uint8_t addr, uint8_t qty, bool=true) { + _emb::_dht_poll(); + _rxPos = 0; _rxLen = 0; + if (qty > 32) qty = 32; + if (addr == 0x68 || addr == 0x69) { + if (_reg == 0x75) { _rx[0] = addr; _rxLen = 1; } + else if (_reg == 0x3B) { + uint8_t a = addr; + float* v = _emb::_imu_values[a]; + int16_t ax = _accelVal(v[0]), ay = _accelVal(v[1]), az = _accelVal(v[2]); + int16_t gx = _gyroVal(v[3]), gy = _gyroVal(v[4]), gz = _gyroVal(v[5]); + int16_t temp = (int16_t)((v[6] + 21.0f) * 340.0f); + _rx[0] = ax >> 8; _rx[1] = ax & 0xFF; + _rx[2] = ay >> 8; _rx[3] = ay & 0xFF; + _rx[4] = az >> 8; _rx[5] = az & 0xFF; + _rx[6] = temp >> 8; _rx[7] = temp & 0xFF; + _rx[8] = gx >> 8; _rx[9] = gx & 0xFF; + _rx[10] = gy >> 8; _rx[11] = gy & 0xFF; + _rx[12] = gz >> 8; _rx[13] = gz & 0xFF; + _rxLen = 14; + } + } else if (addr == 0x76 || addr == 0x77) { + if (_reg == 0xD0) { _rx[0] = 0x60; _rxLen = 1; } + else if (_reg == 0xF7) { + uint8_t a = addr; + float t = _emb::_bme_values[a][0]; + float h = _emb::_bme_values[a][1]; + float p = _emb::_bme_values[a][2]; + uint32_t pp = (uint32_t)((p * 100.0f) * 256.0f); + int32_t tt = (int32_t)((t + 40.0f) * 100.0f * 65536.0f / 165.0f); + uint32_t hh = (uint32_t)(h * 1024.0f / 100.0f); + _rx[0] = (pp >> 16) & 0xFF; _rx[1] = (pp >> 8) & 0xFF; _rx[2] = pp & 0xFF; + _rx[3] = (tt >> 16) & 0xFF; _rx[4] = (tt >> 8) & 0xFF; _rx[5] = tt & 0xFF; + _rx[6] = (hh >> 8) & 0xFF; _rx[7] = hh & 0xFF; + _rxLen = 8; + } + } + return _rxLen; + } + size_t write(uint8_t b) { _reg = b; return 1; } + size_t write(const uint8_t* d, size_t n) { if (n) _reg = d[0]; return n; } + int available() { return _rxLen - _rxPos; } + int read() { return _rxPos < _rxLen ? _rx[_rxPos++] : -1; } + int peek() { return _rxPos < _rxLen ? _rx[_rxPos] : -1; } + void flush() {} + void onReceive(void(*)(int)) {} + void onRequest(void(*)(void)) {} +}; +extern TwoWire Wire; +#endif +`; + +// MPU6050 mock for ESP32 QEMU — covers Jeff Rowberg's I2Cdev/MPU6050 basic API +const _MPU6050_MOCK_H = `#pragma once +#ifndef _EMU_MPU6050_MOCK_H +#define _EMU_MPU6050_MOCK_H +#include "Wire.h" +class MPU6050 { + uint8_t _addr; +public: + MPU6050(uint8_t addr=0x68) : _addr(addr) {} + void initialize() { + Wire.begin(); + Wire.beginTransmission(_addr); + Wire.write(0x6B); + Wire.write(0); + Wire.endTransmission(); + Wire.beginTransmission(_addr); + Wire.requestFrom(_addr, (uint8_t)14); + } + bool testConnection() { return true; } + void getMotion6(int16_t* ax, int16_t* ay, int16_t* az, int16_t* gx, int16_t* gy, int16_t* gz) { + _emb::_dht_poll(); + Wire.beginTransmission(_addr); + Wire.write(0x3B); + Wire.endTransmission(); + Wire.requestFrom(_addr, (uint8_t)14); + *ax = (Wire.read() << 8) | Wire.read(); + *ay = (Wire.read() << 8) | Wire.read(); + *az = (Wire.read() << 8) | Wire.read(); + Wire.read(); Wire.read(); // temp + *gx = (Wire.read() << 8) | Wire.read(); + *gy = (Wire.read() << 8) | Wire.read(); + *gz = (Wire.read() << 8) | Wire.read(); + } + int16_t getTemperature() { + _emb::_dht_poll(); + Wire.beginTransmission(_addr); + Wire.write(0x3B); + Wire.endTransmission(); + Wire.requestFrom(_addr, (uint8_t)8); + for (int i=0;i<6;i++) Wire.read(); + int16_t t = (Wire.read() << 8) | Wire.read(); + return t; + } +}; +#endif +`; + +// BME280 mock for ESP32 QEMU — covers Adafruit_BME280 begin/readTemperature/readPressure/readHumidity +const _BME280_MOCK_H = `#pragma once +#ifndef _EMU_BME280_MOCK_H +#define _EMU_BME280_MOCK_H +#include "Wire.h" +class Adafruit_BME280 { + uint8_t _addr; +public: + Adafruit_BME280() : _addr(0x76) {} + bool begin(uint8_t addr=0x76, TwoWire* wire=nullptr) { + _addr = addr; + (void)wire; + Wire.begin(); + Wire.beginTransmission(_addr); + Wire.write(0xD0); + Wire.endTransmission(false); + Wire.requestFrom(_addr, (uint8_t)1); + Wire.read(); + Wire.beginTransmission(_addr); + Wire.write(0xF4); + Wire.write(0x27); + Wire.endTransmission(); + return true; + } + float readTemperature() { + _emb::_dht_poll(); + return _emb::_bme_values[_addr][0]; + } + float readPressure() { + _emb::_dht_poll(); + return _emb::_bme_values[_addr][2]; + } + float readHumidity() { + _emb::_dht_poll(); + return _emb::_bme_values[_addr][1]; + } + float readAltitude(float seaLevel=1013.25) { + float p = readPressure(); + return 44330.0f * (1.0f - powf(p / seaLevel, 0.1903f)); + } +}; +class BME280 : public Adafruit_BME280 {}; +#endif +`; + +// OneWire mock for ESP32 QEMU +const _ONEWIRE_MOCK_H = `#pragma once +#ifndef _EMU_ONEWIRE_MOCK_H +#define _EMU_ONEWIRE_MOCK_H +#include "gpio_bridge.h" +class OneWire { + uint8_t _pin; + bool _initSent; +public: + OneWire(uint8_t pin) : _pin(pin), _initSent(false) { _emb::_ow_last_pin = pin; } + bool reset(void) { return true; } + void write(uint8_t v, uint8_t p=0) { (void)v; (void)p; } + uint8_t read() { return 0xBE; } + void reset_search() {} + bool search(uint8_t* addr, bool normal=true) { + _emb::_dht_poll(); + _emb::_ow_last_pin = _pin; + if (!_initSent) { + Serial.printf("\\x02OWI:%d\\n", (int)_pin); + _initSent = true; + } + uint8_t id[8] = {0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + memcpy(addr, id, 8); + return true; + } + void select(const uint8_t* addr) { (void)addr; } +}; +#endif +`; + +// DallasTemperature mock for ESP32 QEMU +const _DALLASTEMP_MOCK_H = `#pragma once +#ifndef _EMU_DALLASTEMP_MOCK_H +#define _EMU_DALLASTEMP_MOCK_H +#include "OneWire.h" +class DallasTemperature { + OneWire* _wire; +public: + DallasTemperature(OneWire* wire) : _wire(wire) {} + void begin() {} + void setResolution(uint8_t r) { (void)r; } + void requestTemperatures() {} + float getTempCByIndex(uint8_t idx) { + _emb::_dht_poll(); + int pin = _emb::_ow_last_pin; + return _emb::_ow_t[pin >= 0 && pin < 40 ? pin : 0]; + } + float getTempFByIndex(uint8_t idx) { + float c = getTempCByIndex(idx); + return c * 9.0f / 5.0f + 32.0f; + } +}; +#endif +`; + +// Relay module mock for ESP32 QEMU +const _RELAY_MOCK_H = `#pragma once +#ifndef _EMU_RELAY_MOCK_H +#define _EMU_RELAY_MOCK_H +#include "gpio_bridge.h" +class Relay { + int _pin; + bool _on; +public: + Relay(int pin, bool activeLow=false) : _pin(pin), _on(false) { (void)activeLow; } + void begin() { + pinMode(_pin, OUTPUT); + Serial.printf("\\x02RLY:%d:0\\n", _pin); + } + void on() { _on = true; digitalWrite(_pin, HIGH); Serial.printf("\\x02RLY:%d:1\\n", _pin); } + void off() { _on = false; digitalWrite(_pin, LOW); Serial.printf("\\x02RLY:%d:0\\n", _pin); } + bool isOn() { return _on; } +}; +#endif +`; + +// L298N motor driver mock for ESP32 QEMU +const _L298N_MOCK_H = `#pragma once +#ifndef _EMU_L298N_MOCK_H +#define _EMU_L298N_MOCK_H +#include "gpio_bridge.h" +class L298N { + int _enA, _in1, _in2, _enB, _in3, _in4; + int _speedA, _speedB; + bool _hasB; + void _emit() { + Serial.printf("\\x02MST:%d:%d:%d:%d:%d:%d\\n", _enA, _in1, _in2, _in3, _in4, _enB); + } +public: + L298N(int enA, int in1, int in2, int enB=0, int in3=0, int in4=0) + : _enA(enA), _in1(in1), _in2(in2), _enB(enB), _in3(in3), _in4(in4), _speedA(0), _speedB(0), _hasB(enB>0) { + Serial.printf("\\x02MOT:%d:%d:%d:%d:%d:%d\\n", enA, in1, in2, in3, in4, enB); + } + void begin() { + pinMode(_enA, OUTPUT); pinMode(_in1, OUTPUT); pinMode(_in2, OUTPUT); + if (_hasB) { pinMode(_enB, OUTPUT); pinMode(_in3, OUTPUT); pinMode(_in4, OUTPUT); } + _emit(); + } + void setSpeedA(int s) { _speedA = s; analogWrite(_enA, s); _emit(); } + void setSpeedB(int s) { _speedB = s; if (_hasB) analogWrite(_enB, s); _emit(); } + void forwardA() { digitalWrite(_in1, HIGH); digitalWrite(_in2, LOW); _emit(); } + void backwardA() { digitalWrite(_in1, LOW); digitalWrite(_in2, HIGH); _emit(); } + void stopA() { digitalWrite(_in1, LOW); digitalWrite(_in2, LOW); _emit(); } + void forwardB() { if (_hasB) { digitalWrite(_in3, HIGH); digitalWrite(_in4, LOW); _emit(); } } + void backwardB() { if (_hasB) { digitalWrite(_in3, LOW); digitalWrite(_in4, HIGH); _emit(); } } + void stopB() { if (_hasB) { digitalWrite(_in3, LOW); digitalWrite(_in4, LOW); _emit(); } } + void forward() { forwardA(); if (_hasB) forwardB(); } + void backward() { backwardA(); if (_hasB) backwardB(); } + void stop() { stopA(); if (_hasB) stopB(); } +}; +#endif +`; + +// Mock SPI library — prevents real SPI.cpp (hardware init) from being linked in QEMU +const _SPI_MOCK_H = `#pragma once +#include +class SPISettings { +public: + SPISettings(uint32_t clk=4000000, uint8_t bitOrder=MSBFIRST, uint8_t dataMode=SPI_MODE0) {} +}; +class SPIClass { +public: + SPIClass() {} + void begin(int8_t sck=-1, int8_t miso=-1, int8_t mosi=-1, int8_t ss=-1) {} + void end() {} + void beginTransaction(SPISettings s) {} + void endTransaction() {} + uint8_t transfer(uint8_t d) { return 0; } + uint16_t transfer16(uint16_t d) { return 0; } + void transfer(void *buf, size_t count) {} + void setBitOrder(uint8_t b) {} + void setDataMode(uint8_t m) {} + void setClockDivider(uint32_t d) {} + bool initialized() { return true; } +}; +extern SPIClass SPI; +#define SPI_HAS_TRANSACTION 1 +#define SPI_MODE0 0 +#define SPI_MODE1 1 +#define SPI_MODE2 2 +#define SPI_MODE3 3 +`; + +// Mock TFT library — injected when sketch includes TFT_eSPI / Adafruit_ILI9341 / Adafruit_ST7735 +const _TFT_MOCK_H = `#pragma once +#ifndef _EMU_TFT_MOCK_H +#define _EMU_TFT_MOCK_H +#include "gpio_bridge.h" +#define TFT_BLACK 0x0000 +#define TFT_WHITE 0xFFFF +#define TFT_RED 0xF800 +#define TFT_GREEN 0x07E0 +#define TFT_BLUE 0x001F +#define TFT_CYAN 0x07FF +#define TFT_MAGENTA 0xF81F +#define TFT_YELLOW 0xFFE0 +#define TFT_ORANGE 0xFD20 +#define TFT_DARKGREY 0x7BEF +#define TFT_LIGHTGREY 0xC618 +#define TFT_NAVY 0x000F +#define TFT_DARKGREEN 0x03E0 +#define TFT_DARKCYAN 0x03EF +#define TFT_MAROON 0x7800 +#define TFT_PURPLE 0x780F +#define TFT_OLIVE 0x7BE0 +#define TFT_GREENYELLOW 0xB7E0 +#define TFT_PINK 0xFC18 +#define ILI9341_BLACK TFT_BLACK +#define ILI9341_WHITE TFT_WHITE +#define ILI9341_RED TFT_RED +#define ILI9341_GREEN TFT_GREEN +#define ILI9341_BLUE TFT_BLUE +#define ILI9341_CYAN TFT_CYAN +#define ILI9341_MAGENTA TFT_MAGENTA +#define ILI9341_YELLOW TFT_YELLOW +#define ILI9341_ORANGE TFT_ORANGE +#define ST77XX_BLACK TFT_BLACK +#define ST77XX_WHITE TFT_WHITE +#define ST77XX_RED TFT_RED +#define ST77XX_GREEN TFT_GREEN +#define ST77XX_BLUE TFT_BLUE +#define ST77XX_CYAN TFT_CYAN +#define ST77XX_MAGENTA TFT_MAGENTA +#define ST77XX_YELLOW TFT_YELLOW +#define TL_DATUM 0 +#define TC_DATUM 1 +#define TR_DATUM 2 +namespace _emb_tft { + static int16_t W=240, H=320, cx=0, cy=0; + static uint16_t fg=0xFFFF, bg=0x0000; + static uint8_t ts=1; + static int cmdCnt=0; + static inline void feedWdt(){ if(++cmdCnt>=2){cmdCnt=0;yield();} } +} +class TFT_eSPI : public Print { + int16_t _w, _h; +public: + TFT_eSPI(int16_t w=240, int16_t h=320) : _w(w), _h(h) {} + void begin() { init(); } + void init() { _emb_tft::W=_w; _emb_tft::H=_h; if(Serial) Serial.printf("\\x02TI:%d,%d\\n",(int)_w,(int)_h); } + int16_t width() { return _w; } + int16_t height() { return _h; } + void setRotation(uint8_t r) { + if((r==1||r==3)&&_w!=_h){int16_t t=_w;_w=_h;_h=t;_emb_tft::W=_w;_emb_tft::H=_h;} + if(Serial) Serial.printf("\\x02TR:%d\\n",(int)r); + } + void fillScreen(uint32_t c) { if(Serial) Serial.printf("\\x02TF:%d\\n",(int)(c&0xFFFF)); _emb_tft::feedWdt(); } + void fillRect(int32_t x,int32_t y,int32_t w,int32_t h,uint32_t c) + { if(Serial) Serial.printf("\\x02TB:%d,%d,%d,%d,%d\\n",(int)x,(int)y,(int)w,(int)h,(int)(c&0xFFFF)); _emb_tft::feedWdt(); } + void drawRect(int32_t x,int32_t y,int32_t w,int32_t h,uint32_t c) + { drawFastHLine(x,y,w,c);drawFastHLine(x,y+h-1,w,c);drawFastVLine(x,y,h,c);drawFastVLine(x+w-1,y,h,c); } + void drawPixel(int32_t x,int32_t y,uint32_t c) + { if(Serial) Serial.printf("\\x02TP:%d,%d,%d\\n",(int)x,(int)y,(int)(c&0xFFFF)); } + void drawFastHLine(int32_t x,int32_t y,int32_t w,uint32_t c){ fillRect(x,y,w,1,c); } + void drawFastVLine(int32_t x,int32_t y,int32_t h,uint32_t c){ fillRect(x,y,1,h,c); } + void drawLine(int32_t x0,int32_t y0,int32_t x1,int32_t y1,uint32_t c) + { if(Serial) Serial.printf("\\x02TL:%d,%d,%d,%d,%d\\n",(int)x0,(int)y0,(int)x1,(int)y1,(int)(c&0xFFFF)); } + void drawCircle(int32_t x,int32_t y,int32_t r,uint32_t c) + { if(Serial) Serial.printf("\\x02TC:%d,%d,%d,%d,0\\n",(int)x,(int)y,(int)r,(int)(c&0xFFFF)); } + void fillCircle(int32_t x,int32_t y,int32_t r,uint32_t c) + { if(Serial) Serial.printf("\\x02TC:%d,%d,%d,%d,1\\n",(int)x,(int)y,(int)r,(int)(c&0xFFFF)); } + void fillRoundRect(int32_t x,int32_t y,int32_t w,int32_t h,int32_t r,uint32_t c){ fillRect(x,y,w,h,c); } + void drawRoundRect(int32_t x,int32_t y,int32_t w,int32_t h,int32_t r,uint32_t c){ drawRect(x,y,w,h,c); } + void fillTriangle(int32_t x0,int32_t y0,int32_t x1,int32_t y1,int32_t x2,int32_t y2,uint32_t c) + { drawLine(x0,y0,x1,y1,c);drawLine(x1,y1,x2,y2,c);drawLine(x2,y2,x0,y0,c); } + void setTextColor(uint32_t c){ _emb_tft::fg=(uint16_t)(c&0xFFFF); } + void setTextColor(uint32_t f,uint32_t b){ _emb_tft::fg=(uint16_t)(f&0xFFFF);_emb_tft::bg=(uint16_t)(b&0xFFFF); } + void setTextSize(uint8_t s){ _emb_tft::ts=s; } + void setCursor(int16_t x,int16_t y){ _emb_tft::cx=x;_emb_tft::cy=y; } + void setTextDatum(uint8_t d){} + void setTextPadding(uint16_t x){} + void setTextFont(uint8_t f){} + int16_t getCursorX(){ return _emb_tft::cx; } + int16_t getCursorY(){ return _emb_tft::cy; } + size_t write(uint8_t c) override { + if(c=='\\n'){ _emb_tft::cy+=8*_emb_tft::ts; _emb_tft::cx=0; _emb_tft::feedWdt(); return 1; } + if(c=='\\r') return 1; + if(Serial) Serial.printf("\\x02TT:%d,%d,%d,%d,%d,%d\\n", + (int)_emb_tft::cx,(int)_emb_tft::cy,(int)_emb_tft::fg,(int)_emb_tft::bg,(int)_emb_tft::ts,(int)c); + _emb_tft::cx+=6*_emb_tft::ts; + _emb_tft::feedWdt(); + return 1; + } + void drawString(const char* s,int32_t x,int32_t y,uint8_t f=2){ setCursor(x,y);print(s); } + void drawString(const String& s,int32_t x,int32_t y,uint8_t f=2){ drawString(s.c_str(),x,y,f); } + void drawCentreString(const char* s,int32_t x,int32_t y,uint8_t f=2){ setCursor(x-(int16_t)(strlen(s)*3*_emb_tft::ts),y);print(s); } + void drawNumber(long n,int32_t x,int32_t y,uint8_t f=2){ setCursor(x,y);print(n); } + void drawFloat(float n,uint8_t dp,int32_t x,int32_t y,uint8_t f=2){ setCursor(x,y);print(n,dp); } + int16_t textWidth(const char* s, uint8_t f=2){ return (int16_t)(strlen(s)*6*_emb_tft::ts); } + int16_t textWidth(const String& s, uint8_t f=2){ return textWidth(s.c_str(),f); } + static uint16_t color565(uint8_t r,uint8_t g,uint8_t b){ return ((r&0xF8)<<8)|((g&0xFC)<<3)|(b>>3); } + static uint16_t color24to16(uint32_t c){ return color565((c>>16)&0xFF,(c>>8)&0xFF,c&0xFF); } +}; +class Adafruit_ILI9341 : public TFT_eSPI { +public: + Adafruit_ILI9341(int8_t cs=-1,int8_t dc=-1,int8_t mosi=-1,int8_t clk=-1,int8_t rst=-1,int8_t miso=-1) + : TFT_eSPI(240,320){} + void startWrite(){} + void endWrite(){} +}; +class Adafruit_ST7789 : public TFT_eSPI { +public: + Adafruit_ST7789(int8_t cs=-1,int8_t dc=-1,int8_t rst=-1): TFT_eSPI(240,320){} +}; +class Adafruit_ST7735 : public TFT_eSPI { +public: + Adafruit_ST7735(int8_t cs=-1,int8_t dc=-1,int8_t rst=-1): TFT_eSPI(128,160){} + void initR(uint8_t opts=0){ init(); } + void initB(){ init(); } + void initG(){ init(); } +}; +#endif +`; + +// Parse GPIO bridge protocol from PTY output; returns clean text for serial monitor +function _parsePtyGpio(buf: string, sseConns: Set, gpioBuf?: string[]): { text: string; remainder: string } { + let text = ''; + let i = 0; + while (i < buf.length) { + const esc = buf.indexOf('\x02', i); + if (esc === -1) { + text += buf.slice(i); + return { text, remainder: '' }; + } + text += buf.slice(i, esc); + const nl = buf.indexOf('\n', esc); + if (nl === -1) return { text, remainder: buf.slice(esc) }; // incomplete + const cmd = buf.slice(esc + 1, nl); + const m = cmd.match(/^([GM])(\d+):([^\n]+)/); + if (m) { + const type = m[1], pinN = parseInt(m[2]), val = m[3]; + const evt = type === 'G' + ? { g: { p: pinN, v: parseInt(val) } } + : { g: { p: pinN, m: val } }; + const msg = 'data: ' + JSON.stringify(evt) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('DINI:')) { + const pin = parseInt(cmd.slice(5)); + const msg = 'data: ' + JSON.stringify({ dht: { pin } }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('SRVA:')) { + const pin = parseInt(cmd.slice(5)); + const msg = 'data: ' + JSON.stringify({ servo: { pin } }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('SRVO:')) { + const parts = cmd.slice(5).split(':'); + const pin = parseInt(parts[0]), angle = parseInt(parts[1]); + const msg = 'data: ' + JSON.stringify({ servo_angle: { pin, angle } }) + '\n\n'; + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.charAt(0) === 'T') { + // TFT drawing command: "TI:240,320", "TF:0", "TB:x,y,w,h,color", etc. + const payload = cmd.slice(1); // strip leading 'T' + const msg = 'data: ' + JSON.stringify({ tft: payload }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('IMU:')) { + // MPU-6050 init: IMU::: + const p = cmd.slice(4).split(':'); + const sda = parseInt(p[0] || '-1'), scl = parseInt(p[1] || '-1'), addr = parseInt(p[2] || '0x68'); + const msg = 'data: ' + JSON.stringify({ imu: { sda, scl, addr } }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('IMV:')) { + // MPU-6050 values: IMV:::::::: + const p = cmd.slice(4).split(':'); + const [addr, ax, ay, az, gx, gy, gz, temp] = p.map(v => parseFloat(v) || 0); + const msg = 'data: ' + JSON.stringify({ imu_values: { addr, ax, ay, az, gx, gy, gz, temp } }) + '\n\n'; + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('BMU:')) { + // BME280 init: BMU::: + const p = cmd.slice(4).split(':'); + const sda = parseInt(p[0] || '-1'), scl = parseInt(p[1] || '-1'), addr = parseInt(p[2] || '0x76'); + const msg = 'data: ' + JSON.stringify({ bme: { sda, scl, addr } }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('BMV:')) { + // BME280 values: BMV::::

+ const p = cmd.slice(4).split(':'); + const [addr, t, h, pressure] = p.map(v => parseFloat(v) || 0); + const msg = 'data: ' + JSON.stringify({ bme_values: { addr, t, h, p: pressure } }) + '\n\n'; + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('OWI:')) { + // OneWire/DS18B20 init: OWI: + const pin = parseInt(cmd.slice(4)); + const msg = 'data: ' + JSON.stringify({ onewire: { pin } }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('OWV:')) { + // DS18B20 value: OWV:: + const p = cmd.slice(4).split(':'); + const pin = parseInt(p[0] || '0'), t = parseFloat(p[1] || '25'); + const msg = 'data: ' + JSON.stringify({ ds18b20: { pin, t } }) + '\n\n'; + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('RLY:')) { + // Relay state: RLY:: + const p = cmd.slice(4).split(':'); + const pin = parseInt(p[0] || '0'), state = parseInt(p[1] || '0'); + const msg = 'data: ' + JSON.stringify({ relay: { pin, state } }) + '\n\n'; + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('MOT:')) { + // L298N init: MOT:::::: + const p = cmd.slice(4).split(':').map(v => parseInt(v) || -1); + const [enA, in1, in2, in3, in4, enB] = p; + const msg = 'data: ' + JSON.stringify({ motor: { enA, in1, in2, in3, in4, enB } }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('MST:')) { + // L298N state: MST:::::: + const p = cmd.slice(4).split(':').map(v => parseInt(v) || -1); + const [enA, in1, in2, in3, in4, enB] = p; + const msg = 'data: ' + JSON.stringify({ motor_state: { enA, in1, in2, in3, in4, enB } }) + '\n\n'; + for (const res of sseConns) { try { res.write(msg); } catch {} } + } else if (cmd.startsWith('WIFI:')) { + // WiFi mock event: WIFI: + try { + const wifi = JSON.parse(cmd.slice(5)); + const msg = 'data: ' + JSON.stringify({ wifi }) + '\n\n'; + if (gpioBuf) gpioBuf.push(msg); + for (const res of sseConns) { try { res.write(msg); } catch {} } + } catch {} + } + i = nl + 1; + } + return { text, remainder: '' }; +} + +function killQemuSession(id: string) { + const s = qemuSessions.get(id); + if (!s) return; + qemuSessions.delete(id); + clearTimeout(s.timer); + if (s.pty) { try { s.pty.kill(); } catch {} try { s.pty.stdin?.end(); } catch {} } + try { require('fs').rmSync(s.tmpDir, { recursive: true, force: true }); } catch {} +} + +function _qemuMachine(fqbn: string): string { + const chip = _detectChip(fqbn); + const machine = _qemuForChip(chip); + if (machine) return machine; + if (chip === 'S2') throw new Error('ESP32-S2는 QEMU 미지원 (chip id 0x0002). ⚡ 업로드 버튼으로 실제 보드에 올리세요.'); + if (['C3','C5','C6','H2','P4'].includes(chip)) throw new Error(`ESP32-${chip}(RISC-V)는 QEMU 미지원. ⚡ 업로드 버튼을 사용하세요.`); + throw new Error(`${chip} 칩은 QEMU 미지원`); +} + +function _spawnQemu(fwPath: string, tmpDir: string, fqbn: string): any { + const machine = _qemuMachine(fqbn); + // Use child_process.spawn instead of node-pty to avoid PTY echo issues + // (PTY echoes written data back as output, corrupting \x02 bridge protocol parsing) + const child = require('child_process').spawn(_qemuPath, [ + '-machine', machine, + '-display', 'none', + '-drive', `file=${fwPath},if=mtd,format=raw`, + '-serial', 'stdio', + '-monitor', 'null', + '-global', 'driver=timer.esp32.timg,property=wdt_disable,value=true', + ], { stdio: ['pipe', 'pipe', 'pipe'], cwd: tmpDir }); + return child; +} + +app.post('/api/arduino/qemu/start', async (req, res) => { + // Accepts code+fqbn (preferred) or pre-compiled merged base64 (legacy) + const { code, fqbn: rawFqbn, merged } = req.body || {}; + if (!code && !merged) return res.status(400).json({ error: 'code 또는 merged bin이 필요합니다' }); + + const id = 'qemu_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); + const tmpDir = require('os').tmpdir() + '/' + id; + require('fs').mkdirSync(tmpDir, { recursive: true }); + + const fqbn = rawFqbn && /^[\w\-:\.]+$/.test(rawFqbn) ? rawFqbn : 'esp32:esp32:esp32'; + const fwPath = tmpDir + '/firmware.bin'; + + // Fail fast for unsupported chip variants before expensive compile + try { _qemuMachine(fqbn); } catch (e: any) { + require('fs').rmSync(tmpDir, { recursive: true, force: true }); + return res.json({ error: e.message }); + } + + try { + if (code) { + // Compile with DIO flash mode (QEMU doesn't support QIO) + const cliPath = await new Promise(resolve => { + const candidates = [process.env.HOME + '/.local/bin/arduino-cli', '/usr/local/bin/arduino-cli', '/usr/bin/arduino-cli']; + let i = 0; + const next = () => { + if (i >= candidates.length) { require('child_process').exec('which arduino-cli', { env: { ...process.env, PATH: process.env.PATH + ':' + (process.env.HOME || '') + '/.local/bin' } }, (e: any, o: string) => resolve(e ? null : o.trim())); return; } + const p = candidates[i++]; + require('fs').access(p, require('fs').constants.X_OK, (e: any) => e ? next() : resolve(p)); + }; + next(); + }); + if (!cliPath) { require('fs').rmSync(tmpDir, { recursive: true, force: true }); return res.status(503).json({ error: 'arduino-cli 없음' }); } + + const sketchDir = tmpDir + '/sketch'; + const outDir = tmpDir + '/out'; + require('fs').mkdirSync(sketchDir); require('fs').mkdirSync(outDir); + // Inject GPIO bridge header + prepend include to sketch + require('fs').writeFileSync(sketchDir + '/gpio_bridge.h', _GPIO_BRIDGE_H); + // Inject DHT mock if sketch uses DHT library + if (code.includes('#include ') || code.includes('#include "DHT.h"')) { + require('fs').writeFileSync(sketchDir + '/DHT.h', _DHT_MOCK_H); + } + // Inject Servo mock if sketch uses Servo library + if (code.includes('#include ') || code.includes('#include "Servo.h"') + || code.includes('#include ') || code.includes('#include "ESP32Servo.h"')) { + require('fs').writeFileSync(sketchDir + '/Servo.h', _SERVO_MOCK_H); + require('fs').writeFileSync(sketchDir + '/ESP32Servo.h', '#pragma once\n#include "Servo.h"\n'); + } + let sketchCode = code; + // Inject WiFi mock if sketch uses WiFi library + const usesWifi = code.includes('#include ') || code.includes('#include "WiFi.h"') + || code.includes('WiFi.begin') || code.includes('WiFiClient') || code.includes('WiFiServer'); + if (usesWifi) { + require('fs').writeFileSync(sketchDir + '/WiFi.h', _WIFI_MOCK_H); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "WiFi.h"'); + } + // Inject I2C / 1-Wire / actuator mocks + const usesI2c = code.includes('#include ') || code.includes('#include "Wire.h"') + || code.includes('MPU6050') || code.includes('MPU-6050') || code.includes('BME280') + || code.includes('MPU6050_light') || code.includes('Adafruit_MPU6050'); + const usesOneWire = code.includes('OneWire') || code.includes('DallasTemperature') || code.includes('DS18B20'); + const usesRelay = code.includes('#include ') || code.includes('Relay'); + const usesL298n = code.includes('L298N') || code.includes('L298'); + if (usesI2c) { + require('fs').writeFileSync(sketchDir + '/Wire.h', _WIRE_MOCK_H); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "Wire.h"'); + } + if (code.includes('MPU6050') || code.includes('MPU-6050') || code.includes('Adafruit_MPU6050')) { + require('fs').writeFileSync(sketchDir + '/MPU6050.h', _MPU6050_MOCK_H); + if (code.includes('Adafruit_MPU6050')) { + require('fs').writeFileSync(sketchDir + '/Adafruit_MPU6050.h', '#pragma once\n#include "MPU6050.h"\n'); + } + } + if (code.includes('BME280') || code.includes('bme280')) { + require('fs').writeFileSync(sketchDir + '/BME280.h', _BME280_MOCK_H); + require('fs').writeFileSync(sketchDir + '/Adafruit_BME280.h', '#pragma once\n#include "BME280.h"\n'); + } + if (usesOneWire) { + require('fs').writeFileSync(sketchDir + '/OneWire.h', _ONEWIRE_MOCK_H); + if (code.includes('DallasTemperature')) { + require('fs').writeFileSync(sketchDir + '/DallasTemperature.h', _DALLASTEMP_MOCK_H); + } + sketchCode = sketchCode.replace(/#include\s*/g, '#include "OneWire.h"') + .replace(/#include\s*/g, '#include "DallasTemperature.h"'); + } + if (usesRelay) { + require('fs').writeFileSync(sketchDir + '/Relay.h', _RELAY_MOCK_H); + } + if (usesL298n) { + require('fs').writeFileSync(sketchDir + '/L298N.h', _L298N_MOCK_H); + } + // Inject TFT mock if sketch uses any TFT library + const _TFT_INCLUDE_NAMES = ['TFT_eSPI.h','Adafruit_ILI9341.h','Adafruit_ST7735.h','Adafruit_ST7789.h','Adafruit_GFX.h']; + const usesTft = _TFT_INCLUDE_NAMES.some(n => code.includes(`<${n}>`) || code.includes(`"${n}"`)); + const usesSpi = code.includes('#include ') || code.includes('#include "SPI.h"'); + if (usesTft || usesSpi) { + require('fs').writeFileSync(sketchDir + '/SPI.h', _SPI_MOCK_H); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "SPI.h"'); + } + if (usesTft) { + require('fs').writeFileSync(sketchDir + '/TFT_eSPI.h', _TFT_MOCK_H); + require('fs').writeFileSync(sketchDir + '/Adafruit_ILI9341.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + require('fs').writeFileSync(sketchDir + '/Adafruit_ST7735.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + require('fs').writeFileSync(sketchDir + '/Adafruit_ST7789.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + require('fs').writeFileSync(sketchDir + '/Adafruit_GFX.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + require('fs').writeFileSync(sketchDir + '/User_Setup.h', '#pragma once\n'); + for (const n of _TFT_INCLUDE_NAMES) { + sketchCode = sketchCode.replace(new RegExp('#include\\s*<' + n.replace('.', '\\.') + '>', 'g'), `#include "${n}"`); + } + } + require('fs').writeFileSync(sketchDir + '/sketch.ino', '#include "gpio_bridge.h"\n' + sketchCode); + + // Auto-install core + const coreId = fqbn.split(':').slice(0, 2).join(':'); + const listRaw = await new Promise(r => require('child_process').exec(`"${cliPath}" core list --format json`, { timeout: 10000 }, (_: any, o: string) => r(o || ''))); + const installedIds: string[] = JSON.parse(listRaw || '{}')?.platforms?.map((p: any) => p.id) || []; + if (!installedIds.includes(coreId)) await new Promise(r => require('child_process').exec(`"${cliPath}" core install "${coreId}"`, { timeout: 300000 }, r)); + + const { stderr: compErr } = await new Promise<{ stdout: string; stderr: string }>((r) => + require('child_process').exec( + `"${cliPath}" compile --fqbn ${fqbn} --board-options FlashMode=dio --output-dir ${outDir} ${sketchDir}`, + { timeout: 120000, maxBuffer: 8 * 1024 * 1024 }, + (err: any, stdout: string, stderr: string) => r({ stdout: stdout || '', stderr: err ? (stderr || String(err)) : '' }) + ) + ); + const outFiles = require('fs').readdirSync(outDir); + const mergedFile = outFiles.find((f: string) => f.endsWith('.merged.bin')); + if (!mergedFile) { require('fs').rmSync(tmpDir, { recursive: true, force: true }); return res.json({ error: compErr || '컴파일 실패: merged.bin 없음' }); } + require('fs').copyFileSync(outDir + '/' + mergedFile, fwPath); + } else { + // Legacy: pre-compiled merged bin + const raw = Buffer.from(merged, 'base64'); + const padded = Buffer.alloc(4 * 1024 * 1024, 0xff); + raw.copy(padded); + require('fs').writeFileSync(fwPath, padded); + } + + const pty = _spawnQemu(fwPath, tmpDir, fqbn); + const listeners = new Set<(d: string) => void>(); + const sseConns = new Set(); + const timer = setTimeout(() => killQemuSession(id), 20 * 60 * 1000); + const session: QemuSession = { pty, tmpDir, fqbn, listeners, sseConns, timer, ptyBuf: '', gpioBuf: [] }; + qemuSessions.set(id, session); + pty.stdout.on('data', (d: Buffer) => { + // Buffer incomplete UTF-8 at chunk boundaries (e.g. ° = 0xC2 0xB0 split across chunks) + let buf = session.ptyBuf || ''; + session.ptyBuf = ''; + const combined = Buffer.concat([ + Buffer.from(buf, 'utf8'), + d + ]); + const str = combined.toString('utf8'); + const { text, remainder } = _parsePtyGpio(str, sseConns, session.gpioBuf); + session.ptyBuf = remainder; + if (text) for (const fn of listeners) fn(text); + }); + pty.on('exit', () => { const s = qemuSessions.get(id); if (s && s.pty === pty) killQemuSession(id); }); + res.json({ id }); + } catch (e: any) { + require('fs').rmSync(tmpDir, { recursive: true, force: true }); + res.status(500).json({ error: e.message || 'QEMU 시작 실패' }); + } +}); + +app.get('/api/arduino/qemu/:id/output', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + // Replay buffered GPIO/DHT/TFT events so late-connecting clients see them + for (const msg of s.gpioBuf) { try { res.write(msg); } catch {} } + const send = (d: string) => { + try { res.write('data: ' + JSON.stringify({ t: d }) + '\n\n'); } catch {} + }; + s.listeners.add(send); + s.sseConns.add(res); + req.on('close', () => { s.listeners.delete(send); s.sseConns.delete(res); }); +}); + +app.post('/api/arduino/qemu/:id/input', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { text } = req.body || {}; + if (text) s.pty.stdin.write(text); + res.json({ ok: true }); +}); + +// Soft pause: kill QEMU process but keep session/firmware for later restart +app.post('/api/arduino/qemu/:id/pause', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const p = s.pty; + s.pty = null; // exit 핸들러가 세션을 삭제하지 않도록 먼저 null로 교체 + if (p) { try { p.kill(); } catch {} try { p.stdin?.end(); } catch {} } + res.json({ ok: true }); +}); + +// GPIO input simulation: send serial message that gpio_bridge.h can intercept +app.post('/api/arduino/qemu/:id/gpio', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { pin, level } = req.body || {}; + if (typeof pin !== 'number' || pin < 0 || pin > 39) return res.status(400).json({ error: 'pin 0-39 required' }); + // Send \x03BTN::\n via serial — firmware can read with Serial.readStringUntil + s.pty.stdin.write(`\x03BTN:${pin}:${level ? 1 : 0}\n`); + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/dht', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: '세션 없음' }); + const { pin = 0, temp = 25, humid = 50 } = req.body || {}; + const pinN = Math.max(0, Math.min(39, parseInt(String(pin)))); + s.pty.stdin.write(`\x02DSET:${pinN}:${Number(temp).toFixed(1)}:${Number(humid).toFixed(1)}\n`); + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/imu', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { addr = 0x68, ax = 0, ay = 0, az = 1, gx = 0, gy = 0, gz = 0, temp = 25 } = req.body || {}; + s.pty.stdin.write(`\x02ISET:${Number(addr).toFixed(0)}:${Number(ax).toFixed(2)}:${Number(ay).toFixed(2)}:${Number(az).toFixed(2)}:${Number(gx).toFixed(2)}:${Number(gy).toFixed(2)}:${Number(gz).toFixed(2)}:${Number(temp).toFixed(1)}\n`); + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/bme', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { addr = 0x76, t = 25, h = 50, p = 1013 } = req.body || {}; + s.pty.stdin.write(`\x02BSET:${Number(addr).toFixed(0)}:${Number(t).toFixed(1)}:${Number(h).toFixed(1)}:${Number(p).toFixed(1)}\n`); + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/ds18b20', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { pin = 0, t = 25 } = req.body || {}; + const pinN = Math.max(0, Math.min(39, parseInt(String(pin)))); + s.pty.stdin.write(`\x02OSET:${pinN}:${Number(t).toFixed(2)}\n`); + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/motor', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { enA = -1, in1 = -1, in2 = -1, in3 = -1, in4 = -1, enB = -1 } = req.body || {}; + s.pty.stdin.write(`\x02MSET:${Number(enA).toFixed(0)}:${Number(in1).toFixed(0)}:${Number(in2).toFixed(0)}:${Number(in3).toFixed(0)}:${Number(in4).toFixed(0)}:${Number(enB).toFixed(0)}\n`); + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/wifi', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { cmd, ssid = '', pass = '' } = req.body || {}; + if (cmd === 'connect') { + s.pty.stdin.write(`\x02WSET:connect:${ssid}:${pass}\n`); + } else if (cmd === 'disconnect') { + s.pty.stdin.write(`\x02WSET:disconnect\n`); + } + res.json({ ok: true }); +}); + +app.post('/api/arduino/qemu/:id/restart', (req, res) => { + const s = qemuSessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const oldPty = s.pty; + s.pty = null; // exit 핸들러가 세션을 삭제하지 않도록 먼저 null + if (oldPty) { try { oldPty.kill(); } catch {} try { oldPty.stdin?.end(); } catch {} } + const pty = _spawnQemu(s.tmpDir + '/firmware.bin', s.tmpDir, s.fqbn || 'esp32:esp32:esp32'); + s.pty = pty; s.ptyBuf = ''; s.gpioBuf = []; + pty.stdout.on('data', (d: Buffer) => { + let buf = s.ptyBuf || ''; + s.ptyBuf = ''; + const combined = Buffer.concat([Buffer.from(buf, 'utf8'), d]); + const str = combined.toString('utf8'); + const { text, remainder } = _parsePtyGpio(str, s.sseConns, s.gpioBuf); + s.ptyBuf = remainder; + if (text) for (const fn of s.listeners) fn(text); + }); + pty.on('exit', () => { if (s.pty === pty) killQemuSession(req.params.id); }); + res.json({ ok: true }); +}); + +app.delete('/api/arduino/qemu/:id', (req, res) => { + killQemuSession(req.params.id); + res.json({ ok: true }); +}); + +// ── ESP8266 Native emulator ──────────────────────────────────────────────────── + +function _cleanEsp8266CompileError(stderr: string, tmpDir: string): string { + const safe = tmpDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return stderr + .split('\n') + .filter(l => !l.includes('gpio_bridge_esp8266.h') && !l.includes('esp8266_main.cpp')) + .map(l => l.replace(new RegExp(safe + '/?', 'g'), '').replace(/^sketch\//, '')) + .join('\n') + .trim(); +} + +function killEsp8266Session(id: string) { + const s = esp8266Sessions.get(id); + if (!s) return; + esp8266Sessions.delete(id); + clearTimeout(s.timer); + if (s.proc) { try { s.proc.kill(); } catch {} try { s.proc.stdin?.end(); } catch {} } + try { require('fs').rmSync(s.tmpDir, { recursive: true, force: true }); } catch {} +} + +function _spawnEsp8266Native(binPath: string, tmpDir: string): any { + return require('child_process').spawn(binPath, [], { + stdio: ['pipe', 'pipe', 'pipe'], + cwd: tmpDir, + }); +} + +app.post('/api/arduino/esp8266/start', async (req, res) => { + const { code, fqbn: rawFqbn } = req.body || {}; + if (!code) return res.status(400).json({ error: 'code가 필요합니다' }); + + const fqbn = rawFqbn && /^[\w\-:\.]+$/.test(rawFqbn) ? rawFqbn : 'esp8266:esp8266:nodemcuv2'; + if (_detectChip(fqbn) !== 'ESP8266') return res.status(400).json({ error: 'ESP8266 fqbn이 필요합니다' }); + + const id = 'esp8266_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); + const tmpDir = require('os').tmpdir() + '/' + id; + const fs = require('fs'); + fs.mkdirSync(tmpDir, { recursive: true }); + + const binPath = tmpDir + '/sketch_emu'; + + try { + // Write bridge header + fs.writeFileSync(tmpDir + '/gpio_bridge_esp8266.h', _GPIO_BRIDGE_ESP8266_H); + fs.writeFileSync(tmpDir + '/esp8266_main.cpp', _ESP8266_MAIN_CPP); + + // Inject library stubs based on includes + let sketchCode = code; + if (code.includes('#include ') || code.includes('#include "DHT.h"')) { + fs.writeFileSync(tmpDir + '/DHT.h', _DHT_MOCK_ESP8266_H); + } + if (code.includes('#include ') || code.includes('#include "Servo.h"') + || code.includes('#include ') || code.includes('#include "ESP32Servo.h"')) { + fs.writeFileSync(tmpDir + '/Servo.h', _SERVO_MOCK_ESP8266_H); + fs.writeFileSync(tmpDir + '/ESP32Servo.h', '#pragma once\n#include "Servo.h"\n'); + } + if (code.includes('#include ') || code.includes('#include "SPI.h"')) { + fs.writeFileSync(tmpDir + '/SPI.h', '#pragma once\n#include "gpio_bridge_esp8266.h"\n'); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "SPI.h"'); + } + // Inject I2C / 1-Wire / actuator mocks + const usesI2c8266 = code.includes('#include ') || code.includes('#include "Wire.h"') + || code.includes('MPU6050') || code.includes('MPU-6050') || code.includes('BME280') + || code.includes('MPU6050_light') || code.includes('Adafruit_MPU6050'); + const usesOneWire8266 = code.includes('OneWire') || code.includes('DallasTemperature') || code.includes('DS18B20'); + const usesRelay8266 = code.includes('#include ') || code.includes('Relay'); + const usesL298n8266 = code.includes('L298N') || code.includes('L298'); + if (usesI2c8266) { + fs.writeFileSync(tmpDir + '/Wire.h', _WIRE_MOCK_ESP8266_H); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "Wire.h"'); + } + if (code.includes('MPU6050') || code.includes('MPU-6050') || code.includes('Adafruit_MPU6050')) { + fs.writeFileSync(tmpDir + '/MPU6050.h', _MPU6050_MOCK_ESP8266_H); + if (code.includes('Adafruit_MPU6050')) { + fs.writeFileSync(tmpDir + '/Adafruit_MPU6050.h', '#pragma once\n#include "MPU6050.h"\n'); + } + } + if (code.includes('BME280') || code.includes('bme280')) { + fs.writeFileSync(tmpDir + '/BME280.h', _BME280_MOCK_ESP8266_H); + fs.writeFileSync(tmpDir + '/Adafruit_BME280.h', '#pragma once\n#include "BME280.h"\n'); + } + if (usesOneWire8266) { + fs.writeFileSync(tmpDir + '/OneWire.h', _ONEWIRE_MOCK_ESP8266_H); + if (code.includes('DallasTemperature')) { + fs.writeFileSync(tmpDir + '/DallasTemperature.h', _DALLASTEMP_MOCK_ESP8266_H); + } + sketchCode = sketchCode.replace(/#include\s*/g, '#include "OneWire.h"') + .replace(/#include\s*/g, '#include "DallasTemperature.h"'); + } + if (usesRelay8266) { + fs.writeFileSync(tmpDir + '/Relay.h', _RELAY_MOCK_ESP8266_H); + } + if (usesL298n8266) { + fs.writeFileSync(tmpDir + '/L298N.h', _L298N_MOCK_ESP8266_H); + } + if (code.includes('ESP8266WiFi') || code.includes('#include /g, '#include "ESP8266WiFi.h"'); + } + if (code.includes('HTTPClient') || code.includes('#include /g, '#include "HTTPClient.h"'); + } + if (code.includes('Ticker')) { + fs.writeFileSync(tmpDir + '/Ticker.h', '#pragma once\n#include "gpio_bridge_esp8266.h"\n'); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "Ticker.h"'); + } + if (code.includes('LittleFS') || code.includes('SPIFFS')) { + fs.writeFileSync(tmpDir + '/LittleFS.h', '#pragma once\n#include "gpio_bridge_esp8266.h"\n'); + fs.writeFileSync(tmpDir + '/FS.h', '#pragma once\n#include "gpio_bridge_esp8266.h"\n'); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "LittleFS.h"'); + sketchCode = sketchCode.replace(/#include\s*/g, '#include "FS.h"'); + } + const _TFT_NAMES_8266 = ['TFT_eSPI.h','Adafruit_ILI9341.h','Adafruit_ST7735.h','Adafruit_ST7789.h','Adafruit_GFX.h']; + if (_TFT_NAMES_8266.some(n => code.includes(`<${n}>`) || code.includes(`"${n}"`))) { + fs.writeFileSync(tmpDir + '/TFT_eSPI.h', _TFT_MOCK_ESP8266_H); + fs.writeFileSync(tmpDir + '/Adafruit_ILI9341.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + fs.writeFileSync(tmpDir + '/Adafruit_ST7735.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + fs.writeFileSync(tmpDir + '/Adafruit_ST7789.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + fs.writeFileSync(tmpDir + '/Adafruit_GFX.h', '#pragma once\n#include "TFT_eSPI.h"\n'); + } + fs.writeFileSync(tmpDir + '/sketch.ino', sketchCode); + + // Compile with g++ (retry once on unknown header error — auto-stub) + const compile = (extraFlags = '') => new Promise<{ ok: boolean; err: string }>((resolve) => { + require('child_process').exec( + `g++ -std=c++17 -x c++ -I${tmpDir} -lpthread -o ${binPath} ${tmpDir}/esp8266_main.cpp${extraFlags}`, + { timeout: 60000, maxBuffer: 4 * 1024 * 1024, cwd: tmpDir }, + (err: any, _stdout: string, stderr: string) => resolve({ ok: !err, err: stderr || '' }) + ); + }); + + let result = await compile(); + // Auto-stub unknown headers (up to 5 iterations) + for (let attempt = 0; attempt < 5 && !result.ok; attempt++) { + const m = result.err.match(/fatal error:\s*([^\s:]+\.h):\s*No such file/); + if (!m) break; + const missing = m[1]; + if (!fs.existsSync(tmpDir + '/' + missing)) { + fs.writeFileSync(tmpDir + '/' + missing, `#pragma once\n// auto-stub\n`); + sketchCode = sketchCode.replace(new RegExp('#include\\s*<' + missing.replace('.', '\\.') + '>', 'g'), `#include "${missing}"`); + fs.writeFileSync(tmpDir + '/sketch.ino', sketchCode); + result = await compile(); + } else break; + } + + if (!result.ok) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + return res.json({ error: _cleanEsp8266CompileError(result.err, tmpDir) || 'ESP8266 컴파일 실패' }); + } + + const proc = _spawnEsp8266Native(binPath, tmpDir); + const listeners = new Set<(d: string) => void>(); + const sseConns = new Set(); + const timer = setTimeout(() => killEsp8266Session(id), 20 * 60 * 1000); + const session: Esp8266Session = { proc, tmpDir, binPath, fqbn, listeners, sseConns, timer, ptyBuf: '', gpioBuf: [] }; + esp8266Sessions.set(id, session); + + proc.stdout.on('data', (d: Buffer) => { + let buf = session.ptyBuf || ''; + session.ptyBuf = ''; + const combined = Buffer.concat([Buffer.from(buf, 'utf8'), d]); + const str = combined.toString('utf8'); + const { text, remainder } = _parsePtyGpio(str, sseConns, session.gpioBuf); + session.ptyBuf = remainder; + if (text) for (const fn of listeners) fn(text); + }); + proc.stderr.on('data', (d: Buffer) => { + const text = d.toString('utf8'); + for (const fn of listeners) fn(text); + }); + proc.on('exit', () => { const s = esp8266Sessions.get(id); if (s && s.proc === proc) killEsp8266Session(id); }); + + res.json({ id }); + } catch (e: any) { + try { require('fs').rmSync(tmpDir, { recursive: true, force: true }); } catch {} + res.status(500).json({ error: e.message || 'ESP8266 에뮬레이터 시작 실패' }); + } +}); + +app.get('/api/arduino/esp8266/:id/output', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + for (const msg of s.gpioBuf) { try { res.write(msg); } catch {} } + const send = (d: string) => { + try { res.write('data: ' + JSON.stringify({ t: d }) + '\n\n'); } catch {} + }; + s.listeners.add(send); + s.sseConns.add(res); + req.on('close', () => { s.listeners.delete(send); s.sseConns.delete(res); }); +}); + +app.post('/api/arduino/esp8266/:id/input', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { text } = req.body || {}; + if (text) try { s.proc.stdin.write(text); } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/dht', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: '세션 없음' }); + const { pin = 0, temp = 25, humid = 50 } = req.body || {}; + const pinN = Math.max(0, Math.min(19, parseInt(String(pin)))); + try { s.proc.stdin.write(`\x02DSET:${pinN}:${Number(temp).toFixed(1)}:${Number(humid).toFixed(1)}\n`); } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/imu', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { addr = 0x68, ax = 0, ay = 0, az = 1, gx = 0, gy = 0, gz = 0, temp = 25 } = req.body || {}; + try { s.proc.stdin.write(`\x02ISET:${Number(addr).toFixed(0)}:${Number(ax).toFixed(2)}:${Number(ay).toFixed(2)}:${Number(az).toFixed(2)}:${Number(gx).toFixed(2)}:${Number(gy).toFixed(2)}:${Number(gz).toFixed(2)}:${Number(temp).toFixed(1)}\n`); } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/bme', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { addr = 0x76, t = 25, h = 50, p = 1013 } = req.body || {}; + try { s.proc.stdin.write(`\x02BSET:${Number(addr).toFixed(0)}:${Number(t).toFixed(1)}:${Number(h).toFixed(1)}:${Number(p).toFixed(1)}\n`); } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/ds18b20', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { pin = 0, t = 25 } = req.body || {}; + const pinN = Math.max(0, Math.min(19, parseInt(String(pin)))); + try { s.proc.stdin.write(`\x02OSET:${pinN}:${Number(t).toFixed(2)}\n`); } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/motor', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { enA = -1, in1 = -1, in2 = -1, in3 = -1, in4 = -1, enB = -1 } = req.body || {}; + try { s.proc.stdin.write(`\x02MSET:${Number(enA).toFixed(0)}:${Number(in1).toFixed(0)}:${Number(in2).toFixed(0)}:${Number(in3).toFixed(0)}:${Number(in4).toFixed(0)}:${Number(enB).toFixed(0)}\n`); } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/wifi', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const { cmd, ssid = '', pass = '' } = req.body || {}; + try { + if (cmd === 'connect') { + s.proc.stdin.write(`\x02WSET:connect:${ssid}:${pass}\n`); + } else if (cmd === 'disconnect') { + s.proc.stdin.write(`\x02WSET:disconnect\n`); + } + } catch {} + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/pause', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const p = s.proc; + s.proc = null; + if (p) { try { p.kill(); } catch {} try { p.stdin?.end(); } catch {} } + res.json({ ok: true }); +}); + +app.post('/api/arduino/esp8266/:id/restart', (req, res) => { + const s = esp8266Sessions.get(req.params.id); + if (!s) return res.status(404).json({ error: 'session not found' }); + const oldProc = s.proc; + s.proc = null; + if (oldProc) { try { oldProc.kill(); } catch {} try { oldProc.stdin?.end(); } catch {} } + const proc = _spawnEsp8266Native(s.binPath, s.tmpDir); + s.proc = proc; s.ptyBuf = ''; s.gpioBuf = []; + proc.stdout.on('data', (d: Buffer) => { + let buf = s.ptyBuf || ''; + s.ptyBuf = ''; + const combined = Buffer.concat([Buffer.from(buf, 'utf8'), d]); + const str = combined.toString('utf8'); + const { text, remainder } = _parsePtyGpio(str, s.sseConns, s.gpioBuf); + s.ptyBuf = remainder; + if (text) for (const fn of s.listeners) fn(text); + }); + proc.stderr.on('data', (d: Buffer) => { const t = d.toString('utf8'); for (const fn of s.listeners) fn(t); }); + proc.on('exit', () => { if (s.proc === proc) killEsp8266Session(req.params.id); }); + res.json({ ok: true }); +}); + +app.delete('/api/arduino/esp8266/:id', (req, res) => { + killEsp8266Session(req.params.id); + res.json({ ok: true }); +}); + +// ── Arduino: list connected ports ───────────────────────────────────────────── +app.get('/api/arduino/ports', async (_req, res) => { + const cliPath = await new Promise(resolve => { + const candidates = [ + process.env.HOME + '/.local/bin/arduino-cli', + '/usr/local/bin/arduino-cli', + '/usr/bin/arduino-cli', + ]; + let i = 0; + const next = () => { + if (i >= candidates.length) { require('child_process').exec('which arduino-cli', { env: { ...process.env, PATH: process.env.PATH + ':' + (process.env.HOME || '') + '/.local/bin' } }, (e: any, o: string) => resolve(e ? null : o.trim())); return; } + const p = candidates[i++]; + require('fs').access(p, require('fs').constants.X_OK, (e: any) => e ? next() : resolve(p)); + }; + next(); + }); + if (!cliPath) return res.json({ ports: [] }); + try { + const raw = await new Promise(r => require('child_process').exec(`"${cliPath}" board list --format json`, { timeout: 8000 }, (_: any, o: string) => r(o || ''))); + const parsed = JSON.parse(raw || '{}'); + const ports = (parsed.detected_ports || []).map((p: any) => ({ + address: p.port?.address || '', + label: p.port?.label || p.port?.address || '', + protocol: p.port?.protocol || '', + boards: (p.matching_boards || []).map((b: any) => b.name || '').filter(Boolean), + })).filter((p: any) => p.address && p.protocol === 'serial'); + res.json({ ports }); + } catch { res.json({ ports: [] }); } +}); + +// ── Arduino: compile + upload ───────────────────────────────────────────────── +app.post('/api/arduino/upload', async (req, res) => { + const { code, fqbn: rawFqbn, port } = req.body || {}; + if (!code || !rawFqbn || !port) return res.status(400).json({ error: '코드, FQBN, 포트가 필요합니다' }); + const fqbn = /^[\w\-:\.]+$/.test(rawFqbn) ? rawFqbn : 'arduino:avr:uno'; + + const cliPath = await new Promise(resolve => { + const candidates = [ + process.env.HOME + '/.local/bin/arduino-cli', + '/usr/local/bin/arduino-cli', + '/usr/bin/arduino-cli', + ]; + let i = 0; + const next = () => { + if (i >= candidates.length) { require('child_process').exec('which arduino-cli', { env: { ...process.env, PATH: process.env.PATH + ':' + (process.env.HOME || '') + '/.local/bin' } }, (e: any, o: string) => resolve(e ? null : o.trim())); return; } + const p = candidates[i++]; + require('fs').access(p, require('fs').constants.X_OK, (e: any) => e ? next() : resolve(p)); + }; + next(); + }); + if (!cliPath) return res.status(503).json({ error: 'arduino-cli가 설치되어 있지 않습니다.' }); + + const tmpDir = require('os').tmpdir() + '/arduino_upload_' + Date.now(); + const sketchDir = tmpDir + '/sketch'; + require('fs').mkdirSync(sketchDir, { recursive: true }); + require('fs').writeFileSync(sketchDir + '/sketch.ino', code); + + const execQ = (cmd: string, ms = 120000): Promise<{ stdout: string; stderr: string; err: any }> => + new Promise(r => require('child_process').exec(cmd, { timeout: ms, maxBuffer: 8 * 1024 * 1024 }, (err: any, stdout: string, stderr: string) => r({ stdout: stdout || '', stderr: stderr || '', err }))); + + try { + // Auto-install core if missing + const coreId = fqbn.split(':').slice(0, 2).join(':'); + const listOut = await execQ(`"${cliPath}" core list --format json`); + const installedIds: string[] = JSON.parse(listOut.stdout || '{}')?.platforms?.map((p: any) => p.id) || []; + if (coreId && !installedIds.includes(coreId)) { + await execQ(`"${cliPath}" core install "${coreId}"`, 300000); + } + + const { stdout, stderr, err } = await execQ( + `"${cliPath}" compile --upload --fqbn ${fqbn} -p "${port}" ${sketchDir}`, + 180000 + ); + const combined = (stdout + '\n' + stderr).trim(); + if (err && !combined.toLowerCase().includes('success') && !combined.includes('done uploading')) { + return res.json({ error: combined || '업로드 실패' }); + } + res.json({ success: true, output: combined }); + } finally { + require('fs').rmSync(tmpDir, { recursive: true, force: true }); + } +}); + app.get('/{*path}', (_req, res) => { res.sendFile(path.join(webUiPath, 'index.html')); }); diff --git a/web-ui/arduino-emulator.html b/web-ui/arduino-emulator.html index 630b913..78a2d7d 100644 --- a/web-ui/arduino-emulator.html +++ b/web-ui/arduino-emulator.html @@ -6,12 +6,22 @@ Arduino Emulator + + + @@ -195,21 +321,20 @@ sketch.ino 컴파일 중...

- + + + +
- + +
- 속도 - + 속도 1x +
@@ -220,113 +345,106 @@
- + - DIGITAL - - - - - - - - - - - - - - - + DIGITAL + + + + + + + + + + + + + + + + - - RST + + RST - - + + - + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - ATmega - 328P + + ATmega + 328P - PWR + PWR - - L + + L - - - - ICSP + + + + ICSP - ANALOG / POWER - - - - - - - - - + ANALOG / POWER + + + + + + + + + + + - - - - - + + - + stroke="#c0404088" stroke-width="1.6" fill="none" stroke-linecap="round"/> - + stroke="#c0404088" stroke-width="1.6" stroke-linecap="round"/> - - + + - - + +
-
+ @@ -349,6 +467,111 @@
+ + + + + + + + + + + + + + + + + + + + + + + + @@ -366,6 +589,152 @@ + +
+ + + +
+
+
+ 🔌 회로도 +
+ + + +
+ + + + + + + + +
+
+ +
+
+ + + + + + + + + + + + + 와이어: 클릭→끝점 | R: 회전 | F: 좌우반전 | G: 상하반전 | Del: 삭제 | Esc: 취소 | 우클릭: 편집 + + + + + + + + +
+
+ +
+
+ +
+
+
기본 수동 부품
+
🔌
와이어 (Wire)
두 점을 연결하는 도선
W
+
⊡
저항 (Resistor)
전류 제한. 220Ω, 1kΩ, 10kΩ 등
R
+
⊢⊣
커패시터 (Capacitor)
전하 저장. 100nF, 10µF 등
C
+
출력 부품
+
💡
LED
발광 다이오드
L
+
🔢
7-세그먼트 디스플레이
숫자 표시용 LED 배열
+
🔔
부저 (Buzzer)
PWM 신호로 소리 출력
+
🌈
RGB LED
R/G/B 채널 독립 제어
+
🖥
TFT 컬러 LCD
ILI9341/ST7735/ST7789 SPI 디스플레이
+
입력 부품
+
⊙
푸시버튼 (Push Button)
누르는 동안 회로 연결
B
+
🎛
가변저항 (Potentiometer)
0~5V 아날로그 전압 분배
+
🔀
토글 스위치 (Toggle Switch)
ON/OFF 유지형 스위치
+
☀️
조도 센서 (LDR/CdS)
빛에 따라 저항 변화
+
🌡
NTC 서미스터
온도에 따른 저항 변화
+
반도체
+
🔺
BJT 트랜지스터 (NPN/PNP)
전류 증폭/스위칭
T
+
📐
MOSFET (N채널/P채널)
전압 구동 스위칭
+
▷|
다이오드 (1N4148/1N4007)
단방향 전류
+
⚡
제너 다이오드
정전압 기준
+
집적회로 (IC)
+
🟥
555 타이머 IC
단안정/비안정 멀티바이브레이터
+
🟦
741 연산증폭기 (Op-Amp)
증폭, 비교기, 필터
+
🟩
74 시리즈 로직 게이트
AND/OR/NOT/NAND 등
+
↔️
74HC595 시프트 레지스터
SPI 3선→8비트 병렬 출력
+
🔄
L293D 모터 드라이버
DC모터 H브리지 구동
+
센서 모듈
+
💧
DHT11/DHT22 온습도 센서
디지털 1-wire
+
📡
HC-SR04 초음파 거리 센서
Trig/Echo, 2~400cm
+
👁
PIR 동작 감지 센서
적외선 수동 감지
+
🔴
IR 적외선 송수신
리모컨 수신, 장애물 감지
+
🔵
기울기 센서 (SW-520D)
각도 감지
+
⚙
서보 모터 (Servo)
PWM 제어, SG90/MG996R
+
🔌
릴레이 모듈
5V 코일, NO/NC 스위칭
+
🌡
DS18B20 온도 (1-Wire)
-55~125°C, 방수형 가능
+
📐
MPU-6050 자이로/가속도
I2C, 6축 IMU (0x68)
+
🔄
L298N 모터 드라이버
DC모터 2채널, 2A×2
+
🌤
BME280 환경 센서
I2C, 온도/습도/기압
+
통신 모듈
+
📶
HC-05/HC-06 블루투스
UART 직렬 통신
+
📻
nRF24L01 2.4GHz 무선
SPI 인터페이스
+
🌐
ESP8266 ESP-01 Wi-Fi
UART AT 명령
+
개발 보드
+
🔲
ESP32 DevKit
ESP32 30핀 개발보드
+
🔳
ESP8266 개발보드
NodeMCU / D1 Mini / ESP-01
+
🟧
Arduino UNO R3
ATmega328P · D0-D13, A0-A5
+
🟨
Arduino Nano
ATmega328P · D0-D13, A0-A7
+
🟦
Arduino Mega 2560
ATmega2560 · D0-D53, A0-A15
+
전원 부품
+
+
+5V 전원 (VCC)
아두이노 5V 레일
+
⏚
GND (접지)
기준 전위 0V
+
📌
Arduino 핀 연결
D0~D13, A0~A5, 5V, GND
+
🔋
LM7805 전압 레귤레이터
7~35V→5V, 1A
+
+
+ +
+
+ SPICE 넷리스트 + + + + +
+
+ +
+
▶ ngspice 실행하면
노드 전압과 전류가
여기에 표시됩니다.
+
+
+
+
+
-
+
DIGITAL PINS
@@ -399,55 +768,8 @@
- -
-
- 🔌 브레드보드 & SPICE -
- - -
- -
-
- -
-
- 도구: - - - - - - - - - - - - -
-
- -
-
- -
-
- SPICE 넷리스트 - - - -
-
- -
-
▶ DC 시뮬레이션을 실행하면
노드 전압과 소자 상태가
여기에 표시됩니다.
-
-
-
-
-
+ + diff --git a/web-ui/code.html b/web-ui/code.html index 4f3f229..9ff6d52 100644 --- a/web-ui/code.html +++ b/web-ui/code.html @@ -16,13 +16,13 @@ - - - - + + + + - +