v3.0.3: 아두이노 업로더 통합 — 팝업 제거, 코드 에디터 하단 패널로 이전
- arduino-upload.html 삭제, 모든 로직을 code.js로 이식 - ⚡ 업로드 버튼 → 인라인 패널 오픈 (팝업·sessionStorage 릴레이 제거) - OUTPUT / 옵션 / 시리얼 탭, 드래그 리사이즈, ESP32·AVR·STM32 업로드 - server-v2.ts 중복 /api/settings/paths 라우트 제거 (설정 저장 오류 수정) - 업로드 패널 색상·폰트 개선 (배경 밝게, 12~13px) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+750
-52
@@ -1004,13 +1004,7 @@ function csbUploadToBoard() {
|
||||
if (!file || !file.name.endsWith('.ino')) {
|
||||
codeAiAppend('system', '⚠️ .ino 파일을 선택해주세요'); return;
|
||||
}
|
||||
const content = file.model ? file.model.getValue() : (file.content || '');
|
||||
sessionStorage.setItem('arduino_upload_code', content);
|
||||
sessionStorage.setItem('arduino_upload_fqbn', _csbSelBoard.fqbn);
|
||||
sessionStorage.setItem('arduino_upload_board', _csbSelBoard.name);
|
||||
sessionStorage.setItem('arduino_upload_file', file.name);
|
||||
const w = window.open('/arduino-upload.html', '_blank');
|
||||
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨');
|
||||
cupOpenWith(file);
|
||||
}
|
||||
|
||||
function codePreviewRefresh() {
|
||||
@@ -4226,9 +4220,26 @@ function setCodeSbTab(tab) {
|
||||
// ── 라이브러리/보드 매니저 (code editor sidebar) ──────────────────────────────
|
||||
let _csbLibCurrentTab = 'installed';
|
||||
let _csbLibFilterMode = 'all'; // 'all' | 'compat' | 'incompat'
|
||||
let _csbLibViewMode = 'flat'; // 'flat' | 'category'
|
||||
let _csbLibArchFilter = ''; // '' = 전체, 'esp32' | 'esp8266' | 'avr' ...
|
||||
let _csbLibCache = [];
|
||||
let _csbArdSection = 'lib';
|
||||
|
||||
const _CAT_KO = {
|
||||
'Communication': '통신',
|
||||
'Data Processing': '데이터 처리',
|
||||
'Data Storage': '데이터 저장',
|
||||
'Device Control': '장치 제어',
|
||||
'Display': '디스플레이',
|
||||
'Encryption': '암호화',
|
||||
'Imaging': '이미징',
|
||||
'Other': '기타',
|
||||
'Sensors': '센서',
|
||||
'Signal Input/Output':'신호 입출력',
|
||||
'Timing': '타이밍',
|
||||
'Uncategorized': '미분류',
|
||||
};
|
||||
|
||||
function csbArdSection(sec) {
|
||||
_csbArdSection = sec;
|
||||
['lib','board'].forEach(s => {
|
||||
@@ -4269,6 +4280,48 @@ function csbLibFilter(mode) {
|
||||
_csbLibRenderInstalled(_csbLibCache);
|
||||
}
|
||||
|
||||
function csbLibView(mode) {
|
||||
_csbLibViewMode = mode;
|
||||
['flat','category'].forEach(m => {
|
||||
const btn = document.getElementById('csb-libv-' + m);
|
||||
if (!btn) return;
|
||||
const active = m === mode;
|
||||
btn.style.background = active ? 'var(--brand)' : 'var(--panel-2)';
|
||||
btn.style.color = active ? '#fff' : 'var(--muted)';
|
||||
btn.style.borderColor = active ? 'var(--brand)' : 'var(--line)';
|
||||
});
|
||||
_csbLibRenderInstalled(_csbLibCache);
|
||||
}
|
||||
|
||||
function _csbLibSetArch(arch) {
|
||||
_csbLibArchFilter = arch;
|
||||
_csbLibUpdateArchChips(_csbLibCache);
|
||||
_csbLibRenderInstalled(_csbLibCache);
|
||||
}
|
||||
|
||||
function _csbLibUpdateArchChips(libs) {
|
||||
const archRow = document.getElementById('csb-lib-arch-row');
|
||||
if (!archRow) return;
|
||||
// Count non-wildcard archs
|
||||
const archCount = {};
|
||||
for (const l of libs) {
|
||||
for (const a of (l.architectures || [])) {
|
||||
if (a !== '*') archCount[a] = (archCount[a] || 0) + 1;
|
||||
}
|
||||
}
|
||||
// Show archs with ≥2 libs, sorted by count desc
|
||||
const major = Object.entries(archCount).filter(([,c]) => c >= 2).sort((a,b) => b[1]-a[1]).map(([a]) => a);
|
||||
if (!major.length) { archRow.style.display = 'none'; return; }
|
||||
const all = ['', ...major];
|
||||
archRow.innerHTML = all.map(a => {
|
||||
const label = a ? a.toUpperCase() : '전체';
|
||||
const cnt = a ? (archCount[a] || 0) : libs.length;
|
||||
const active = _csbLibArchFilter === a;
|
||||
return `<button onclick="_csbLibSetArch('${a}')" style="font-size:9px;padding:1px 6px;border-radius:3px;border:1px solid ${active?'var(--brand)':'var(--line)'};background:${active?'var(--brand)':'var(--panel-2)'};color:${active?'#fff':'var(--muted)'};cursor:pointer;font-family:inherit" title="${cnt}개">${label}</button>`;
|
||||
}).join('');
|
||||
archRow.style.display = 'flex';
|
||||
}
|
||||
|
||||
function _csbLibListEl() { return document.getElementById('csb-lib-list'); }
|
||||
function _csbLibStatus(msg) { const el = document.getElementById('csb-lib-status'); if (el) el.textContent = msg; }
|
||||
function _csbEsc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
||||
@@ -4280,6 +4333,8 @@ async function csbLibLoad() {
|
||||
const r = await fetch('/api/arduino/libs?all=1' + fqbn, { headers: authHeaders() });
|
||||
const data = await r.json();
|
||||
_csbLibCache = data.libs || [];
|
||||
_csbLibArchFilter = '';
|
||||
_csbLibUpdateArchChips(_csbLibCache);
|
||||
_csbLibRenderInstalled(_csbLibCache);
|
||||
} catch { _csbLibListEl().innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">오류</div>'; }
|
||||
}
|
||||
@@ -4308,70 +4363,238 @@ function _csbLibRenderInstalled(libs) {
|
||||
const el = _csbLibListEl();
|
||||
if (!libs.length) { el.innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">설치된 라이브러리 없음</div>'; return; }
|
||||
|
||||
// Apply filter
|
||||
// Apply board compat filter
|
||||
let filtered = libs;
|
||||
if (_csbLibFilterMode === 'compat') filtered = libs.filter(l => _csbIsCompat(l));
|
||||
if (_csbLibFilterMode === 'incompat') filtered = libs.filter(l => !_csbIsCompat(l));
|
||||
|
||||
// Sort: platform/core first, then compatible, then incompatible
|
||||
if (_csbLibFilterMode === 'all') {
|
||||
filtered = [...libs].sort((a, b) => {
|
||||
const pa = a.location === 'platform', pb = b.location === 'platform';
|
||||
if (pa && !pb) return -1;
|
||||
if (!pa && pb) return 1;
|
||||
const ca = _csbIsCompat(a), cb = _csbIsCompat(b);
|
||||
if (ca && !cb) return -1;
|
||||
if (!ca && cb) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
// Apply architecture filter
|
||||
if (_csbLibArchFilter) {
|
||||
filtered = filtered.filter(l => {
|
||||
const archs = l.architectures || [];
|
||||
return archs.includes('*') || archs.length === 0 || archs.includes(_csbLibArchFilter) ||
|
||||
archs.some(a => a.startsWith(_csbLibArchFilter) || _csbLibArchFilter.startsWith(a));
|
||||
});
|
||||
}
|
||||
|
||||
// Dedup by name when a specific arch or compat filter is active
|
||||
// Priority: user-installed > platform, specific arch > wildcard *, newer version
|
||||
if (_csbLibArchFilter || _csbLibFilterMode === 'compat') {
|
||||
const byName = new Map();
|
||||
for (const l of filtered) {
|
||||
if (!byName.has(l.name)) { byName.set(l.name, []); }
|
||||
byName.get(l.name).push(l);
|
||||
}
|
||||
filtered = [...byName.values()].map(group => group.reduce((best, l) => {
|
||||
if (l.location === 'user' && best.location !== 'user') return l;
|
||||
if (best.location === 'user' && l.location !== 'user') return best;
|
||||
const lWild = !(l.architectures||[]).length || (l.architectures||[]).includes('*');
|
||||
const bWild = !(best.architectures||[]).length || (best.architectures||[]).includes('*');
|
||||
if (!lWild && bWild) return l;
|
||||
if (lWild && !bWild) return best;
|
||||
return (l.version||'') >= (best.version||'') ? l : best;
|
||||
}));
|
||||
}
|
||||
|
||||
// Sort: platform/core first, then compatible, then name
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const pa = a.location === 'platform', pb = b.location === 'platform';
|
||||
if (pa && !pb) return -1;
|
||||
if (!pa && pb) return 1;
|
||||
const ca = _csbIsCompat(a), cb = _csbIsCompat(b);
|
||||
if (ca && !cb) return -1;
|
||||
if (!ca && cb) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
const msg = _csbLibFilterMode === 'compat' ? `${_csbSelBoard.name}과 호환되는 라이브러리 없음` : '비호환 라이브러리 없음';
|
||||
const msg = _csbLibFilterMode === 'compat' ? `${_csbSelBoard.name}과 호환되는 라이브러리 없음`
|
||||
: _csbLibArchFilter ? `${_csbLibArchFilter.toUpperCase()} 라이브러리 없음`
|
||||
: '비호환 라이브러리 없음';
|
||||
el.innerHTML = `<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">${msg}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_csbLibViewMode === 'category') { _csbLibRenderGrouped(filtered); return; }
|
||||
|
||||
const fqbn = _csbSelBoard?.fqbn || '';
|
||||
el.innerHTML = filtered.map(l => {
|
||||
const sid = 'csbex_' + l.name.replace(/\W/g,'_');
|
||||
const archs = l.architectures || [];
|
||||
const compat = _csbIsCompat(l);
|
||||
const isPlatform = l.location === 'platform';
|
||||
const incompatWarn = !compat ? `<div style="font-size:10px;color:#f87171;margin-top:3px">⚠ 현재 보드(${_csbEsc(_csbSelBoard.name)})와 호환되지 않을 수 있음</div>` : '';
|
||||
const platformBadge = isPlatform ? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:#1a2a3a;color:#60a5fa;border:1px solid #2d4a6a">코어</span>` : '';
|
||||
const exRows = (l.examples||[]).map(ex =>
|
||||
`<div style="display:flex;align-items:center;padding:3px 0;gap:6px">
|
||||
<span style="font-size:10px;color:var(--muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">📄 ${_csbEsc(ex)}</span>
|
||||
<button onclick="csbLibOpenExample('${_csbEsc(l.name)}','${_csbEsc(ex)}',${compat},'${_csbEsc(fqbn)}')"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:${compat?'var(--brand)':'#f87171'};border-radius:4px;cursor:pointer;font-family:inherit;white-space:nowrap"
|
||||
title="${compat?'':'⚠ 비호환 예제 — 열면 AI가 자동으로 변환해드립니다'}">
|
||||
${compat?'열기':'열기 ⚠'}</button>
|
||||
</div>`).join('');
|
||||
const hasEx = l.examples?.length > 0;
|
||||
return `<div style="padding:8px 10px;border-bottom:1px solid var(--line)${!compat?';border-left:2px solid #7f1d1d':''}">
|
||||
<div style="display:flex;align-items:flex-start;gap:6px">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap">
|
||||
<span style="font-size:11px;font-weight:700;color:var(--text)">${_csbEsc(l.name)}</span>
|
||||
${platformBadge}${_csbArchBadge(archs)}
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--muted);margin-top:1px">${_csbEsc(l.author||'')} · v${_csbEsc(l.version||'?')}${l.category?' · '+_csbEsc(l.category):''}</div>
|
||||
${l.sentence?`<div style="font-size:10px;color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${_csbEsc(l.sentence)}</div>`:''}
|
||||
${incompatWarn}
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;flex-shrink:0">
|
||||
${hasEx?`<button onclick="document.getElementById('${sid}').style.display=document.getElementById('${sid}').style.display==='none'?'block':'none'"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:var(--muted);border-radius:4px;cursor:pointer;font-family:inherit">예제</button>`:''}
|
||||
${isPlatform?'':`<button onclick="csbLibUninstall('${_csbEsc(l.name)}')"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:#f87171;border-radius:4px;cursor:pointer;font-family:inherit">삭제</button>`}
|
||||
el.innerHTML = _csbBuiltinCard() + filtered.map(l => _csbLibCard(l, fqbn)).join('');
|
||||
}
|
||||
|
||||
function _csbLibCard(l, fqbn) {
|
||||
const sid = 'csbex_' + l.name.replace(/\W/g,'_');
|
||||
const archs = l.architectures || [];
|
||||
const compat = _csbIsCompat(l);
|
||||
const isPlatform = l.location === 'platform';
|
||||
const incompatWarn = !compat ? `<div style="font-size:10px;color:#f87171;margin-top:3px">⚠ 현재 보드(${_csbEsc(_csbSelBoard.name)})와 호환되지 않을 수 있음</div>` : '';
|
||||
const platformBadge = isPlatform ? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:#1a2a3a;color:#60a5fa;border:1px solid #2d4a6a">코어</span>` : '';
|
||||
const exRows = (l.examples||[]).map(ex =>
|
||||
`<div style="display:flex;align-items:center;padding:3px 0;gap:6px">
|
||||
<span style="font-size:10px;color:var(--muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">📄 ${_csbEsc(ex)}</span>
|
||||
<button onclick="csbLibOpenExample('${_csbEsc(l.name)}','${_csbEsc(ex)}',${compat},'${_csbEsc(fqbn)}')"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:${compat?'var(--brand)':'#f87171'};border-radius:4px;cursor:pointer;font-family:inherit;white-space:nowrap"
|
||||
title="${compat?'':'⚠ 비호환 예제 — 열면 AI가 자동으로 변환해드립니다'}">${compat?'열기':'열기 ⚠'}</button>
|
||||
</div>`).join('');
|
||||
const hasEx = l.examples?.length > 0;
|
||||
return `<div style="padding:8px 10px;border-bottom:1px solid var(--line)${!compat?';border-left:2px solid #7f1d1d':''}">
|
||||
<div style="display:flex;align-items:flex-start;gap:6px">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap">
|
||||
<span style="font-size:11px;font-weight:700;color:var(--text)">${_csbEsc(l.name)}</span>
|
||||
${platformBadge}${_csbArchBadge(archs)}
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--muted);margin-top:1px">${_csbEsc(l.author||'')} · v${_csbEsc(l.version||'?')}${l.category?' · '+_csbEsc(l.category):''}</div>
|
||||
${l.sentence?`<div style="font-size:10px;color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${_csbEsc(l.sentence)}</div>`:''}
|
||||
${incompatWarn}
|
||||
</div>
|
||||
${hasEx?`<div id="${sid}" style="display:none;margin-top:5px;padding:5px 8px;background:var(--panel-2);border-radius:5px">${exRows}</div>`:''}
|
||||
<div style="display:flex;gap:4px;flex-shrink:0">
|
||||
${hasEx?`<button onclick="document.getElementById('${sid}').style.display=document.getElementById('${sid}').style.display==='none'?'block':'none'"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:var(--muted);border-radius:4px;cursor:pointer;font-family:inherit">예제</button>`:''}
|
||||
${isPlatform?'':`<button onclick="csbLibUninstall('${_csbEsc(l.name)}')"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:#f87171;border-radius:4px;cursor:pointer;font-family:inherit">삭제</button>`}
|
||||
</div>
|
||||
</div>
|
||||
${hasEx?`<div id="${sid}" style="display:none;margin-top:5px;padding:5px 8px;background:var(--panel-2);border-radius:5px">${exRows}</div>`:''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _csbLibRenderGrouped(libs) {
|
||||
const el = _csbLibListEl();
|
||||
const fqbn = _csbSelBoard?.fqbn || '';
|
||||
// Group by category
|
||||
const groups = {};
|
||||
for (const l of libs) {
|
||||
const cat = l.category || 'Uncategorized';
|
||||
if (!groups[cat]) groups[cat] = [];
|
||||
groups[cat].push(l);
|
||||
}
|
||||
// Sort categories: put Uncategorized/Other last
|
||||
const last = new Set(['Uncategorized', 'Other', '']);
|
||||
const sorted = Object.keys(groups).sort((a,b) => {
|
||||
if (last.has(a) && !last.has(b)) return 1;
|
||||
if (!last.has(a) && last.has(b)) return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
el.innerHTML = _csbBuiltinCard() + sorted.map(cat => {
|
||||
const ko = _CAT_KO[cat] || cat;
|
||||
const gid = 'csbg_' + cat.replace(/\W/g,'_');
|
||||
const cards = groups[cat].map(l => _csbLibCard(l, fqbn)).join('');
|
||||
const cnt = groups[cat].length;
|
||||
return `<div>
|
||||
<div onclick="const b=document.getElementById('${gid}');b.style.display=b.style.display==='none'?'block':'none'"
|
||||
style="display:flex;align-items:center;gap:6px;padding:6px 10px;cursor:pointer;background:var(--panel-2);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:1">
|
||||
<span style="font-size:11px;font-weight:700;color:var(--text)">${_csbEsc(ko)}</span>
|
||||
${cat !== ko ? `<span style="font-size:9px;color:var(--muted)">${_csbEsc(cat)}</span>` : ''}
|
||||
<span style="font-size:10px;color:var(--muted);margin-left:auto">${cnt}개</span>
|
||||
</div>
|
||||
<div id="${gid}">${cards}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const _BUILTIN_EX = [
|
||||
{ name:'Blink.ino', desc:'LED 깜빡이기', code:
|
||||
`void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(LED_BUILTIN, HIGH);
|
||||
delay(1000);
|
||||
digitalWrite(LED_BUILTIN, LOW);
|
||||
delay(1000);
|
||||
}` },
|
||||
{ name:'Button.ino', desc:'버튼으로 LED 제어', code:
|
||||
`const int BTN = 0;
|
||||
const int LED = LED_BUILTIN;
|
||||
|
||||
void setup() {
|
||||
pinMode(BTN, INPUT_PULLUP);
|
||||
pinMode(LED, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(LED, digitalRead(BTN) == LOW ? HIGH : LOW);
|
||||
}` },
|
||||
{ name:'AnalogRead.ino', desc:'아날로그 읽기', code:
|
||||
`void setup() {
|
||||
Serial.begin(115200);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int val = analogRead(A0);
|
||||
Serial.println(val);
|
||||
delay(200);
|
||||
}` },
|
||||
{ name:'Fade.ino', desc:'LED 밝기 조절 (PWM)', code:
|
||||
`const int LED = LED_BUILTIN;
|
||||
int brightness = 0, step = 5;
|
||||
|
||||
void setup() {
|
||||
pinMode(LED, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
analogWrite(LED, brightness);
|
||||
brightness += step;
|
||||
if (brightness <= 0 || brightness >= 255) step = -step;
|
||||
delay(15);
|
||||
}` },
|
||||
{ name:'BlinkWithoutDelay.ino', desc:'딜레이 없는 깜빡이기', code:
|
||||
`const int LED = LED_BUILTIN;
|
||||
bool state = false;
|
||||
unsigned long prev = 0;
|
||||
|
||||
void setup() {
|
||||
pinMode(LED, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
unsigned long now = millis();
|
||||
if (now - prev >= 1000) {
|
||||
prev = now;
|
||||
state = !state;
|
||||
digitalWrite(LED, state);
|
||||
}
|
||||
}` },
|
||||
{ name:'SerialHello.ino', desc:'시리얼 출력', code:
|
||||
`void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("Hello, World!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (Serial.available()) {
|
||||
String s = Serial.readStringUntil('\\n');
|
||||
Serial.print("받음: ");
|
||||
Serial.println(s);
|
||||
}
|
||||
}` },
|
||||
];
|
||||
|
||||
function csbLibOpenBuiltin(idx) {
|
||||
const ex = _BUILTIN_EX[idx];
|
||||
if (!ex) return;
|
||||
codeNewFile(ex.name, ex.code);
|
||||
setCodeSbTab('files');
|
||||
}
|
||||
|
||||
function _csbBuiltinCard() {
|
||||
const rows = _BUILTIN_EX.map((ex, i) =>
|
||||
`<div style="display:flex;align-items:center;padding:3px 0;gap:6px">
|
||||
<span style="font-size:10px;color:var(--muted);flex:1">📄 ${_csbEsc(ex.desc)}</span>
|
||||
<button onclick="csbLibOpenBuiltin(${i})"
|
||||
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:var(--brand);border-radius:4px;cursor:pointer;font-family:inherit;white-space:nowrap">열기</button>
|
||||
</div>`).join('');
|
||||
return `<div style="padding:8px 10px;border-bottom:2px solid var(--line);border-left:3px solid #facc15">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
|
||||
<span style="font-size:11px;font-weight:700;color:var(--text)">Arduino 기본 예제</span>
|
||||
<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:#2a2010;color:#facc15;border:1px solid #4a3a10">기본</span>
|
||||
</div>
|
||||
<div style="margin-top:4px;padding:5px 8px;background:var(--panel-2);border-radius:5px">${rows}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function csbLibOpenExample(libName, exName, isCompat = true, fqbn = '') {
|
||||
_csbLibStatus(`예제 로딩: ${exName}...`);
|
||||
try {
|
||||
@@ -5596,6 +5819,481 @@ _esp32CodeChannel.onmessage = (e) => {
|
||||
_esp32SyncBusy = false;
|
||||
};
|
||||
|
||||
// ── Upload Panel (⚡) ─────────────────────────────────────────────────────────
|
||||
let _cupOpen = false;
|
||||
let _cupPort = null;
|
||||
let _cupBusy = false;
|
||||
let _cupCode = '', _cupFqbn = '', _cupBoardName = '', _cupFname = '';
|
||||
let _cupCompiledData = null;
|
||||
let _cupBoardOptions = [];
|
||||
let _cupSelOptions = {};
|
||||
let _cupMonActive = false;
|
||||
let _cupMonReader = null;
|
||||
let _cupMonWriter = null;
|
||||
let _cupMonBuf = '';
|
||||
const _cupDecoder = new TextDecoder();
|
||||
|
||||
function toggleUploadPanel() {
|
||||
const panel = document.getElementById('code-upload-panel');
|
||||
if (!panel) return;
|
||||
_cupOpen = !_cupOpen;
|
||||
panel.style.display = _cupOpen ? 'flex' : 'none';
|
||||
const btn = document.getElementById('code-upload-btn');
|
||||
if (btn) btn.classList.toggle('active', _cupOpen);
|
||||
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
|
||||
}
|
||||
|
||||
function cupOpenWith(file) {
|
||||
_cupCode = file.model ? file.model.getValue() : (file.content || '');
|
||||
_cupFqbn = _csbSelBoard?.fqbn || 'arduino:avr:uno';
|
||||
_cupBoardName = _csbSelBoard?.name || _cupFqbn;
|
||||
_cupFname = file.name;
|
||||
_cupSelOptions = {};
|
||||
const panel = document.getElementById('code-upload-panel');
|
||||
if (!_cupOpen) {
|
||||
_cupOpen = true;
|
||||
panel.style.display = 'flex';
|
||||
const btn = document.getElementById('code-upload-btn');
|
||||
if (btn) btn.classList.add('active');
|
||||
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
|
||||
}
|
||||
cupTab('log');
|
||||
document.getElementById('cup-tab-log').innerHTML = '';
|
||||
const method = _cupGetUploadMethod(_cupFqbn);
|
||||
_cupUpdateInfoRows();
|
||||
document.getElementById('cup-stm32-hint').style.display = method === 'stm32-uart' ? '' : 'none';
|
||||
document.getElementById('cup-uf2-hint').style.display = method === 'download-uf2' ? '' : 'none';
|
||||
document.getElementById('cup-port-name').textContent = _cupPort ? '✓ 포트 선택됨' : '포트 없음';
|
||||
document.getElementById('cup-btn-upload').disabled = !_cupPort;
|
||||
document.getElementById('cup-btn-download').disabled = !_cupPort;
|
||||
cupLog(`보드: ${_cupBoardName}`, 'info');
|
||||
cupLog(`방식: ${_cupMethodLabel(method)}`, 'info');
|
||||
if (method === 'stm32-uart') cupLog('STM32: BOOT0=HIGH 후 RESET → ⚡ 업로드', 'warn');
|
||||
else if (method === 'avr-stk500') cupLog('AVR: 포트 선택 후 ⚡ 업로드 (자동 DTR 리셋)', 'info');
|
||||
else if (method === 'download-uf2' || method === 'download-bin') cupLog('이 보드는 ⬇ 다운로드 후 수동 복사 방식입니다.', 'warn');
|
||||
else if (method === 'esp32-flash') cupLog('ESP32/ESP8266: 🔌 포트 선택 후 ⚡ 업로드\n 컴파일: 서버 | 플래싱: 브라우저 esptool-js', 'info');
|
||||
if (!navigator.serial) cupLog('⚠ WebSerial 미지원 브라우저입니다. Chrome/Edge를 사용하세요.', 'warn');
|
||||
_cupLoadBoardOptions();
|
||||
}
|
||||
|
||||
function cupTab(name) {
|
||||
const tabs = { log: 'block', opts: 'block', mon: 'flex' };
|
||||
Object.keys(tabs).forEach(t => {
|
||||
const p = document.getElementById('cup-tab-' + t);
|
||||
const b = document.getElementById('cup-tbtn-' + t);
|
||||
if (p) p.style.display = t === name ? tabs[t] : 'none';
|
||||
if (b) { b.style.color = t === name ? '#e89040' : '#8a7050'; b.style.borderBottom = t === name ? '2px solid #e89040' : '2px solid transparent'; }
|
||||
});
|
||||
}
|
||||
|
||||
function cupLog(msg, cls = 'info') {
|
||||
const out = document.getElementById('cup-tab-log');
|
||||
if (!out) return;
|
||||
const colors = { info: '#9aacb8', ok: '#5dc48a', err: '#e07070', prog: '#80a8e0', warn: '#e8c860' };
|
||||
const span = document.createElement('span');
|
||||
span.style.color = colors[cls] || '#6a7880';
|
||||
span.style.display = 'block';
|
||||
span.textContent = msg;
|
||||
out.appendChild(span);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
|
||||
function _cupSetStatus(cls, text) {
|
||||
const el = document.getElementById('cup-status');
|
||||
if (!el) return;
|
||||
const styles = { idle:{ bg:'#2a1806', color:'#6a5030' }, ready:{ bg:'#1a3020', color:'#4d9e74' }, compiling:{ bg:'#2a1e08', color:'#ccaa50' }, uploading:{ bg:'#2a1208', color:'#c87d30' }, done:{ bg:'#1a3020', color:'#4d9e74' }, error:{ bg:'#2a1010', color:'#c46060' } };
|
||||
const s = styles[cls] || styles.idle;
|
||||
el.style.background = s.bg; el.style.color = s.color; el.textContent = text;
|
||||
}
|
||||
|
||||
function _cupSetProgress(pct) {
|
||||
const bar = document.getElementById('cup-progress');
|
||||
if (!bar) return;
|
||||
if (pct == null) { bar.style.display = 'none'; return; }
|
||||
bar.style.display = 'block'; bar.value = pct;
|
||||
}
|
||||
|
||||
function _cupGetUploadMethod(fqbn) {
|
||||
const arch = fqbn.split(':')[1] || '', vendor = fqbn.split(':')[0] || '';
|
||||
if (arch === 'avr') return 'avr-stk500';
|
||||
if (vendor === 'STMicroelectronics') return 'stm32-uart';
|
||||
if (arch === 'samd' || arch === 'sam') return 'download-bin';
|
||||
if (arch === 'mbed_rp2040' || arch === 'rp2040') return 'download-uf2';
|
||||
if (arch === 'esp32' || arch === 'esp8266') return 'esp32-flash';
|
||||
return 'download-bin';
|
||||
}
|
||||
|
||||
function _cupMethodLabel(m) {
|
||||
return { 'avr-stk500':'STK500 (WebSerial)', 'stm32-uart':'STM32 UART', 'download-bin':'.bin 다운로드', 'download-uf2':'UF2 드래그&드롭', 'esp32-flash':'esptool-js (브라우저)' }[m] || m;
|
||||
}
|
||||
|
||||
function _cupGetEffectiveFqbn() {
|
||||
const keys = Object.keys(_cupSelOptions);
|
||||
if (!keys.length) return _cupFqbn;
|
||||
return _cupFqbn + ':' + keys.map(k => k + '=' + _cupSelOptions[k]).join(',');
|
||||
}
|
||||
|
||||
function _cupUpdateInfoRows() {
|
||||
const el = document.getElementById('cup-info-rows');
|
||||
if (!el) return;
|
||||
const method = _cupGetUploadMethod(_cupFqbn);
|
||||
el.innerHTML = [['방식', _cupMethodLabel(method)], ['보드', _cupBoardName], ['FQBN', _cupGetEffectiveFqbn()], ['파일', _cupFname]]
|
||||
.map(([k, v]) => `<div style="display:flex;gap:10px;font-size:13px"><span style="color:#907050;min-width:40px">${k}</span><span style="color:#c8a060;word-break:break-all">${v}</span></div>`).join('');
|
||||
}
|
||||
|
||||
async function _cupLoadBoardOptions() {
|
||||
const baseFqbn = _cupFqbn.split(':').slice(0, 3).join(':');
|
||||
try {
|
||||
const r = await fetch(`/api/arduino/board/options?fqbn=${encodeURIComponent(baseFqbn)}`, { credentials: 'include', headers: authHeaders() });
|
||||
const d = await r.json();
|
||||
_cupBoardOptions = d.options || []; _cupSelOptions = {};
|
||||
_cupRenderBoardOptions();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function _cupRenderBoardOptions() {
|
||||
const list = document.getElementById('cup-board-opts-list');
|
||||
if (!list) return;
|
||||
if (!_cupBoardOptions.length) { list.innerHTML = ''; return; }
|
||||
list.innerHTML = '<div style="font-size:12px;font-weight:700;color:#907050;margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em">보드 옵션</div>' +
|
||||
_cupBoardOptions.map(o => {
|
||||
const sel = _cupSelOptions[o.id] ?? (o.values.find(v => v.selected)?.value ?? o.values[0]?.value ?? '');
|
||||
const opts = o.values.map(v => `<option value="${v.value}"${v.value === sel ? ' selected' : ''}>${v.label}</option>`).join('');
|
||||
const optId = JSON.stringify(o.id), defVal = o.values.find(v => v.selected)?.value ?? o.values[0]?.value ?? '';
|
||||
return `<div style="display:flex;align-items:center;gap:8px;padding:4px 0"><span style="color:#907050;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px" title="${o.label}">${o.label}</span><select style="background:#3a2818;border:1px solid #5a3820;color:#c8a060;border-radius:4px;padding:3px 6px;font-size:12px;font-family:'IBM Plex Mono',monospace;flex:1.4" onchange="if(this.value!==this.dataset.def)_cupSelOptions[${optId}]=this.value;else delete _cupSelOptions[${optId}];_cupUpdateInfoRows()" data-def="${defVal}">${opts}</select></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function cupSelectPort() {
|
||||
if (!navigator.serial) { cupLog('❌ WebSerial 미지원. Chrome/Edge를 사용하세요.', 'err'); return; }
|
||||
try {
|
||||
_cupPort = await navigator.serial.requestPort();
|
||||
const info = _cupPort.getInfo();
|
||||
const vid = info.usbVendorId ? `VID:0x${info.usbVendorId.toString(16).padStart(4,'0')}` : '';
|
||||
const pid = info.usbProductId ? `PID:0x${info.usbProductId.toString(16).padStart(4,'0')}` : '';
|
||||
const label = [vid, pid].filter(Boolean).join(' ') || '선택됨';
|
||||
document.getElementById('cup-port-name').textContent = '✓ ' + label;
|
||||
document.getElementById('cup-btn-upload').disabled = false;
|
||||
document.getElementById('cup-btn-download').disabled = false;
|
||||
cupLog(`포트 선택됨: ${label}`, 'ok');
|
||||
_cupSetStatus('ready', '준비됨');
|
||||
} catch(e) { if (e.name !== 'NotFoundError') cupLog('포트 선택 오류: ' + e.message, 'err'); }
|
||||
}
|
||||
|
||||
async function _cupCompile() {
|
||||
const arch = _cupFqbn.split(':')[1] || '';
|
||||
if (/ESP8266WiFi|ESP8266WebServer|ESP8266HTTPClient/i.test(_cupCode) && arch !== 'esp8266') {
|
||||
_cupFqbn = 'esp8266:esp8266:nodemcuv2'; _cupSelOptions = {};
|
||||
cupLog(`⚠ ESP8266 감지 → FQBN 자동 변경: ${_cupFqbn}`, 'warn'); _cupUpdateInfoRows(); _cupLoadBoardOptions();
|
||||
} else if (/^#include\s+<WiFi\.h>/m.test(_cupCode) && arch !== 'esp32' && arch !== 'esp8266') {
|
||||
_cupFqbn = 'esp32:esp32:esp32'; _cupSelOptions = {};
|
||||
cupLog(`⚠ ESP32 WiFi 감지 → FQBN 자동 변경: ${_cupFqbn}`, 'warn'); _cupUpdateInfoRows(); _cupLoadBoardOptions();
|
||||
}
|
||||
cupLog('서버에서 컴파일 중...', 'prog'); _cupSetStatus('compiling', '컴파일 중...');
|
||||
const r = await fetch('/api/arduino/compile', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify({ code: _cupCode, fqbn: _cupGetEffectiveFqbn() }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (data.error) { cupLog('컴파일 오류:\n' + data.error, 'err'); _cupSetStatus('error', '컴파일 실패'); return null; }
|
||||
const kb = data.size ? ` (${(data.size/1024).toFixed(1)} KB)` : '';
|
||||
cupLog(`✅ 컴파일 완료${kb}`, 'ok');
|
||||
return data;
|
||||
}
|
||||
|
||||
async function cupDoDownload() {
|
||||
cupTab('log');
|
||||
const data = await _cupCompile(); if (!data) return;
|
||||
_cupCompiledData = data;
|
||||
const isHex = !!data.hex;
|
||||
const content = isHex ? data.hex : _cupBase64ToBytes(data.bin || data.merged);
|
||||
const ext = data.boardType === 'uf2' ? '.uf2' : (isHex ? '.hex' : '.bin');
|
||||
const blob = new Blob([content], { type: isHex ? 'text/plain' : 'application/octet-stream' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = _cupFname.replace(/\.[^.]+$/, '') + ext;
|
||||
a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||
cupLog(`다운로드: ${a.download}`, 'ok'); _cupSetStatus('done', '다운로드됨');
|
||||
}
|
||||
|
||||
async function cupDoUpload() {
|
||||
if (_cupBusy) return;
|
||||
if (!_cupPort) { cupLog('포트를 먼저 선택하세요 (🔌 포트 버튼)', 'err'); return; }
|
||||
cupTab('log');
|
||||
const method = _cupGetUploadMethod(_cupGetEffectiveFqbn());
|
||||
if (method === 'esp32-flash') { await _cupUploadESP32(); return; }
|
||||
if (method === 'download-uf2' || method === 'download-bin') { await cupDoDownload(); return; }
|
||||
_cupBusy = true;
|
||||
document.getElementById('cup-btn-upload').disabled = true;
|
||||
document.getElementById('cup-btn-download').disabled = true;
|
||||
document.getElementById('cup-tab-log').innerHTML = '';
|
||||
_cupSetProgress(0);
|
||||
try {
|
||||
const data = await _cupCompile(); if (!data) return;
|
||||
_cupCompiledData = data;
|
||||
cupLog(`\n--- 업로드 시작 [${_cupBoardName}] ---`, 'prog'); _cupSetStatus('uploading', '업로드 중...'); _cupSetProgress(5);
|
||||
if (method === 'avr-stk500') await _cupUploadAVR(data.hex);
|
||||
else if (method === 'stm32-uart') await _cupUploadSTM32(_cupBase64ToBytes(data.bin));
|
||||
} catch(e) { cupLog('❌ 오류: ' + e.message, 'err'); _cupSetStatus('error', '업로드 실패'); _cupSetProgress(null); }
|
||||
finally { _cupBusy = false; document.getElementById('cup-btn-upload').disabled = false; document.getElementById('cup-btn-download').disabled = false; }
|
||||
}
|
||||
|
||||
async function _cupUploadESP32() {
|
||||
_cupBusy = true;
|
||||
document.getElementById('cup-btn-upload').disabled = true;
|
||||
document.getElementById('cup-btn-download').disabled = true;
|
||||
document.getElementById('cup-tab-log').innerHTML = '';
|
||||
_cupSetProgress(0);
|
||||
let transport = null, success = false;
|
||||
try {
|
||||
const data = await _cupCompile(); if (!data) return;
|
||||
_cupCompiledData = data;
|
||||
const b64 = data.merged || data.bin;
|
||||
if (!b64) throw new Error('컴파일 결과에 바이너리가 없습니다');
|
||||
cupLog('esptool-js 로딩 중...', 'prog');
|
||||
const { ESPLoader, Transport, HardReset } = await import('/esptool-bundle.js');
|
||||
cupLog('보드 연결 중 (자동 리셋)...', 'prog'); _cupSetStatus('uploading', '연결 중...');
|
||||
transport = new Transport(_cupPort, true);
|
||||
const terminal = { clean:()=>{}, writeLine:m=>{ if(m.trim()) cupLog(m,'info'); }, write:m=>{ if(m.trim()) cupLog(m,'info'); } };
|
||||
const esploader = new ESPLoader({ transport, baudrate:115200, romBaudrate:115200, enableTracing:false, terminal });
|
||||
let chip;
|
||||
try { chip = await esploader.main(); }
|
||||
catch(e) { cupLog('⚠ 자동 리셋 실패 — BOOT 누른채 RST → BOOT 놓기 후 재시도.', 'warn'); throw e; }
|
||||
cupLog(`칩 감지: ${chip}`, 'ok'); _cupSetProgress(10); _cupSetStatus('uploading', '업로드 중...');
|
||||
const binData = _cupBase64ToBytes(b64);
|
||||
let _t0 = Date.now(), _w0 = 0;
|
||||
await esploader.writeFlash({
|
||||
fileArray: [{ data: binData, address: 0x0 }],
|
||||
flashSize:'keep', flashMode:'keep', flashFreq:'keep', eraseAll:false, compress:true,
|
||||
reportProgress: (_fi, written, total) => {
|
||||
const pct = Math.round(written/total*100); _cupSetProgress(10 + pct*0.88);
|
||||
const now = Date.now();
|
||||
if (_w0===0 && written>0) { _t0=now; _w0=written; }
|
||||
const kbps = (now-_t0)>200 ? ((written-_w0)/1024/((now-_t0)/1000)).toFixed(1) : '...';
|
||||
document.getElementById('cup-port-name').textContent = `플래싱 ${(written/1024).toFixed(1)}/${(total/1024).toFixed(1)} KB (${pct}%) · ${kbps} KB/s`;
|
||||
},
|
||||
});
|
||||
try { await new HardReset(transport).reset(); } catch {}
|
||||
cupLog('✅ 업로드 완료! 보드가 재시작됩니다.', 'ok'); _cupSetStatus('done', '완료'); _cupSetProgress(100); success = true;
|
||||
} catch(e) { cupLog('❌ 오류: ' + e.message, 'err'); _cupSetStatus('error', '업로드 실패'); _cupSetProgress(null); }
|
||||
finally {
|
||||
try { if (transport) await transport.disconnect(); } catch {}
|
||||
_cupBusy = false;
|
||||
document.getElementById('cup-btn-upload').disabled = false;
|
||||
document.getElementById('cup-btn-download').disabled = false;
|
||||
document.getElementById('cup-port-name').textContent = _cupPort ? '✓ 포트 선택됨' : '포트 없음';
|
||||
setTimeout(() => _cupSetProgress(null), 3000);
|
||||
}
|
||||
if (success) {
|
||||
cupTab('mon'); _cupMonAppendSys('[업로드 완료 — 시리얼 모니터 자동 연결 중...]');
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
await cupMonOpen();
|
||||
}
|
||||
}
|
||||
|
||||
function _cupBase64ToBytes(b64) {
|
||||
const raw = atob(b64), out = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function _cupParseIntelHex(hexText) {
|
||||
const pages = new Map(); let extAddr = 0;
|
||||
for (const line of hexText.split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t.startsWith(':') || t.length < 11) continue;
|
||||
const bytes = []; for (let i=1; i<t.length-1; i+=2) bytes.push(parseInt(t.substr(i,2),16));
|
||||
const len=bytes[0], addr=(bytes[1]<<8)|bytes[2], type=bytes[3];
|
||||
if (type===1) break; if (type===4) { extAddr=((bytes[4]<<8)|bytes[5])<<16; continue; } if (type!==0) continue;
|
||||
const fullAddr = extAddr+addr;
|
||||
for (let i=0; i<len; i++) { const a=fullAddr+i, pg=a>>7, off=a&0x7f; if (!pages.has(pg)) pages.set(pg,new Uint8Array(128).fill(0xff)); pages.get(pg)[off]=bytes[4+i]; }
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
async function _cupWrite(writer, data) { await writer.write(data instanceof Uint8Array ? data : new Uint8Array(Array.isArray(data) ? data : [data])); }
|
||||
|
||||
async function _cupRead(reader, n, ms=5000) {
|
||||
const buf=new Uint8Array(n); let pos=0; const deadline=Date.now()+ms;
|
||||
while (pos<n) {
|
||||
if (Date.now()>deadline) throw new Error(`시리얼 타임아웃 (${n}B 중 ${pos}B)`);
|
||||
const {value,done} = await reader.read(); if (done) throw new Error('포트가 닫혔습니다');
|
||||
const take=Math.min(value.length,n-pos); buf.set(value.slice(0,take),pos); pos+=take;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
async function _cupUploadAVR(hexText) {
|
||||
const INSYNC=0x14,OK=0x10,EOP=0x20,CMD_SYNC=0x30,CMD_SET_DEV=0x42,CMD_ENTER=0x50,CMD_LEAVE=0x51,CMD_ADDR=0x55,CMD_PROG=0x64;
|
||||
await _cupPort.open({ baudRate:115200, dataBits:8, stopBits:1, parity:'none' });
|
||||
const reader=_cupPort.readable.getReader(), writer=_cupPort.writable.getWriter();
|
||||
const cmdOk = async (bytes,extra=0,ms=3000) => {
|
||||
await _cupWrite(writer, new Uint8Array([...bytes,EOP]));
|
||||
const r=await _cupRead(reader,2+extra,ms);
|
||||
if (r[0]!==INSYNC) throw new Error(`STK500: INSYNC 없음 (0x${r[0].toString(16)})`);
|
||||
if (r[1+extra]!==OK) throw new Error(`STK500: OK 없음 (0x${r[1+extra].toString(16)})`);
|
||||
return r.slice(1,1+extra);
|
||||
};
|
||||
try {
|
||||
if (_cupPort.setSignals) {
|
||||
await _cupPort.setSignals({ dataTerminalReady:false, requestToSend:false });
|
||||
await new Promise(r=>setTimeout(r,100));
|
||||
await _cupPort.setSignals({ dataTerminalReady:true, requestToSend:true });
|
||||
await new Promise(r=>setTimeout(r,50));
|
||||
await _cupPort.setSignals({ dataTerminalReady:false });
|
||||
}
|
||||
await new Promise(r=>setTimeout(r,250));
|
||||
cupLog('동기화 중...', 'prog');
|
||||
let synced=false;
|
||||
for (let i=0; i<12&&!synced; i++) { try { await _cupWrite(writer,[CMD_SYNC,EOP]); const r=await _cupRead(reader,2,300); if(r[0]===INSYNC&&r[1]===OK) synced=true; } catch {} }
|
||||
if (!synced) throw new Error('보드에 연결할 수 없습니다. 케이블/DTR 리셋을 확인하세요.');
|
||||
cupLog('✓ 동기화됨', 'ok');
|
||||
await cmdOk([CMD_SET_DEV,0x86,0x00,0x07,0x7f,0x83,0xff,0x00,0x01,0x01,0x01,0x01,0x03,0xff,0x00,0x80,0x04,0x00,0x00,0x00,0x80,0x00]);
|
||||
await cmdOk([CMD_ENTER]); cupLog('✓ 프로그래밍 모드 진입', 'ok');
|
||||
const pages=_cupParseIntelHex(hexText), sorted=[...pages.entries()].sort((a,b)=>a[0]-b[0]);
|
||||
let done=0;
|
||||
for (const [pg, pgData] of sorted) {
|
||||
const waddr=pg<<6; await cmdOk([CMD_ADDR,waddr&0xff,(waddr>>8)&0xff]); await cmdOk([CMD_PROG,0x00,0x80,0x46,...pgData]);
|
||||
done++; _cupSetProgress(5+Math.round(done/sorted.length*90));
|
||||
document.getElementById('cup-port-name').textContent = `업로드 중... ${done}/${sorted.length} 페이지`;
|
||||
}
|
||||
await cmdOk([CMD_LEAVE]); _cupSetProgress(100); cupLog(`✅ 업로드 완료! (${sorted.length} 페이지)`, 'ok');
|
||||
_cupSetStatus('done', '완료 ✓'); document.getElementById('cup-port-name').textContent = '✓ 포트 선택됨';
|
||||
await new Promise(r=>setTimeout(r,500)); _cupSetProgress(null);
|
||||
} finally { try{reader.releaseLock();}catch{} try{writer.releaseLock();}catch{} try{await _cupPort.close();}catch{} }
|
||||
}
|
||||
|
||||
async function _cupUploadSTM32(binData) {
|
||||
const ACK=0x79;
|
||||
await _cupPort.open({ baudRate:115200, dataBits:8, stopBits:1, parity:'even' });
|
||||
const reader=_cupPort.readable.getReader(), writer=_cupPort.writable.getWriter();
|
||||
const w = async d=>_cupWrite(writer,d), readByte=async(ms=6000)=>(await _cupRead(reader,1,ms))[0];
|
||||
const waitAck = async(ms=12000)=>{ const b=await readByte(ms); if(b===ACK)return; if(b===0x1F)throw new Error('STM32 NACK'); throw new Error(`응답: 0x${b.toString(16)}`); };
|
||||
const xor4=a=>a[0]^a[1]^a[2]^a[3], addrPkt=addr=>{ const b=[(addr>>24)&0xff,(addr>>16)&0xff,(addr>>8)&0xff,addr&0xff]; return new Uint8Array([...b,xor4(b)]); };
|
||||
try {
|
||||
cupLog('STM32 동기화 중...', 'prog'); cupLog('(BOOT0=HIGH 상태에서 RESET 확인)', 'warn');
|
||||
let synced=false;
|
||||
for (let i=0; i<5&&!synced; i++) { try{await w([0x7F]);const b=await readByte(1200);if(b===ACK||b===0x1F)synced=true;}catch{} }
|
||||
if (!synced) throw new Error('동기화 실패 — BOOT0=HIGH 후 RESET 하세요.');
|
||||
cupLog('✓ 동기화됨', 'ok');
|
||||
await w([0x00,0xFF]); await waitAck(3000); const n=await readByte(); await _cupRead(reader,n+1,3000); await waitAck(3000);
|
||||
cupLog('플래시 지우는 중... (최대 30초)', 'prog'); _cupSetProgress(10);
|
||||
await w([0x44,0xBB]); await waitAck(5000); await w([0xFF,0xFF,0x00]); await waitAck(30000);
|
||||
cupLog('✓ 플래시 지움', 'ok'); _cupSetProgress(20);
|
||||
const START=0x08000000, CHUNK=256, total=Math.ceil(binData.length/CHUNK);
|
||||
cupLog(`${binData.length} 바이트 쓰는 중 (${total} 블록)...`, 'prog');
|
||||
for (let i=0; i<binData.length; i+=CHUNK) {
|
||||
const raw=binData.slice(i,i+CHUNK), padded=new Uint8Array(CHUNK).fill(0xff); padded.set(raw);
|
||||
await w([0x31,0xCE]); await waitAck(5000); await w(addrPkt(START+i)); await waitAck(5000);
|
||||
let ck=CHUNK-1; for(const b of padded)ck^=b;
|
||||
const pkt=new Uint8Array(CHUNK+2); pkt[0]=CHUNK-1; pkt.set(padded,1); pkt[CHUNK+1]=ck;
|
||||
await w(pkt); await waitAck(10000);
|
||||
const blk=Math.floor(i/CHUNK)+1; _cupSetProgress(20+Math.round(blk/total*75));
|
||||
document.getElementById('cup-port-name').textContent = `업로드 중... ${blk}/${total} 블록`;
|
||||
}
|
||||
_cupSetProgress(97); cupLog('실행 중...', 'prog');
|
||||
await w([0x21,0xDE]); await waitAck(5000); await w(addrPkt(START)); try{await waitAck(2000);}catch{}
|
||||
_cupSetProgress(100); cupLog('✅ 업로드 완료! BOOT0=GND 후 RESET 하세요.', 'ok');
|
||||
_cupSetStatus('done', '완료 ✓'); document.getElementById('cup-port-name').textContent = '✓ 포트 선택됨';
|
||||
await new Promise(r=>setTimeout(r,500)); _cupSetProgress(null);
|
||||
} finally { try{reader.releaseLock();}catch{} try{writer.releaseLock();}catch{} try{await _cupPort.close();}catch{} }
|
||||
}
|
||||
|
||||
// ── Serial monitor ───────────────────────────────────────────────────────────
|
||||
function _cupMonAppendSys(msg) {
|
||||
const out = document.getElementById('cup-mon-output'); if (!out) return;
|
||||
const div = document.createElement('div');
|
||||
div.style.color = '#907050'; div.style.fontStyle = 'italic'; div.textContent = msg;
|
||||
out.appendChild(div);
|
||||
if (document.getElementById('cup-mon-auto')?.checked) out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
|
||||
function cupMonAppend(text) {
|
||||
const out = document.getElementById('cup-mon-output'); if (!out) return;
|
||||
const showTs = document.getElementById('cup-mon-ts')?.checked;
|
||||
const autoScroll = document.getElementById('cup-mon-auto')?.checked;
|
||||
_cupMonBuf += text;
|
||||
const parts = _cupMonBuf.split('\n'); _cupMonBuf = parts.pop();
|
||||
for (const raw of parts) {
|
||||
const div = document.createElement('div'); div.style.color = '#c8e8c0';
|
||||
if (showTs) {
|
||||
const d=new Date(), ts=`${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}:${d.getSeconds().toString().padStart(2,'0')}.${d.getMilliseconds().toString().padStart(3,'0')}`;
|
||||
const s=document.createElement('span'); s.style.cssText='color:#907050;font-size:11px;margin-right:6px;opacity:.7;user-select:none'; s.textContent=ts;
|
||||
div.appendChild(s); div.appendChild(document.createTextNode(raw.replace(/\r/g,'')));
|
||||
} else { div.textContent = raw.replace(/\r/g,''); }
|
||||
out.appendChild(div);
|
||||
}
|
||||
if (autoScroll) out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
|
||||
async function _cupMonReadLoop() {
|
||||
while (_cupMonActive && _cupMonReader) {
|
||||
try { const {value,done}=await _cupMonReader.read(); if(done)break; cupMonAppend(_cupDecoder.decode(value,{stream:true})); }
|
||||
catch(e) { if (_cupMonActive) _cupMonAppendSys(`[오류: ${e.message}]`); break; }
|
||||
}
|
||||
if (_cupMonActive) cupMonClose();
|
||||
}
|
||||
|
||||
async function cupMonToggle() { if (_cupMonActive) await cupMonClose(); else await cupMonOpen(); }
|
||||
|
||||
async function cupMonOpen() {
|
||||
if (!_cupPort) { _cupMonAppendSys('[포트를 먼저 선택하세요]'); return; }
|
||||
if (_cupBusy) { _cupMonAppendSys('[업로드 중입니다. 완료 후 연결하세요]'); return; }
|
||||
const baud = parseInt(document.getElementById('cup-mon-baud')?.value) || 115200;
|
||||
try {
|
||||
await _cupPort.open({ baudRate: baud });
|
||||
_cupMonReader = _cupPort.readable.getReader();
|
||||
_cupMonWriter = _cupPort.writable.getWriter();
|
||||
_cupMonActive = true;
|
||||
const btn = document.getElementById('cup-mon-btn');
|
||||
if (btn) { btn.textContent='■ 연결 해제'; btn.style.color='#e87070'; btn.style.borderColor='color-mix(in srgb,#e87070 35%,#241a0e)'; }
|
||||
const inp = document.getElementById('cup-mon-input'); if (inp) inp.disabled = false;
|
||||
_cupMonAppendSys(`[연결됨 — ${baud} baud]`); _cupMonReadLoop();
|
||||
} catch(e) { _cupMonAppendSys(`[연결 실패: ${e.message}]`); }
|
||||
}
|
||||
|
||||
async function cupMonClose() {
|
||||
_cupMonActive = false;
|
||||
try{await _cupMonReader?.cancel();}catch{} try{_cupMonReader?.releaseLock();}catch{}
|
||||
try{_cupMonWriter?.releaseLock();}catch{} try{await _cupPort?.close();}catch{}
|
||||
_cupMonReader=null; _cupMonWriter=null;
|
||||
const btn=document.getElementById('cup-mon-btn');
|
||||
if (btn) { btn.textContent='▶ 연결'; btn.style.color='#4ade80'; btn.style.borderColor='color-mix(in srgb,#4ade80 35%,#241a0e)'; }
|
||||
const inp=document.getElementById('cup-mon-input'); if(inp) inp.disabled=true;
|
||||
_cupMonAppendSys('[연결 해제됨]');
|
||||
}
|
||||
|
||||
async function cupMonSend() {
|
||||
if (!_cupMonWriter) return;
|
||||
const input=document.getElementById('cup-mon-input'); if(!input) return;
|
||||
const endings={none:'',nl:'\n',cr:'\r',crnl:'\r\n'};
|
||||
const suffix=endings[document.getElementById('cup-mon-line-end')?.value]??'\r\n';
|
||||
const text=input.value+suffix; input.value='';
|
||||
try {
|
||||
await _cupMonWriter.write(new TextEncoder().encode(text));
|
||||
const div=document.createElement('div'); div.style.color='#94b0d8'; div.textContent=text.replace(/[\r\n]/g,'');
|
||||
document.getElementById('cup-mon-output')?.appendChild(div);
|
||||
} catch(e) { _cupMonAppendSys(`[전송 오류: ${e.message}]`); }
|
||||
}
|
||||
|
||||
function cupMonClear() { const el=document.getElementById('cup-mon-output'); if(el) el.innerHTML=''; _cupMonBuf=''; }
|
||||
|
||||
// Upload panel resize drag
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const drag = document.getElementById('code-upload-drag');
|
||||
const panel = document.getElementById('code-upload-panel');
|
||||
if (!drag || !panel) return;
|
||||
let startY=0, startH=0;
|
||||
drag.addEventListener('mousedown', e => {
|
||||
startY=e.clientY; startH=panel.offsetHeight; e.preventDefault();
|
||||
const onMove = e2 => { const dy=startY-e2.clientY; panel.style.height=Math.max(80,Math.min(window.innerHeight*.6,startH+dy))+'px'; };
|
||||
const onUp = () => { document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); drag.style.background=''; };
|
||||
drag.addEventListener('mouseover', ()=>drag.style.background='rgba(200,125,48,.4)');
|
||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
});
|
||||
|
||||
// Here we run them once the DOM is ready since this page IS the code view.
|
||||
(function bootCodePage() {
|
||||
async function boot() {
|
||||
|
||||
Reference in New Issue
Block a user