feat: STM32 Renode 에뮬레이터 작동 — HAL 폴링 루프 + 비트밴드 + 타이밍 수정
- stm32f103_fixed.repl: Miscellaneous.BitBanding 추가로 HAL bit-band RCC 접근(0x42xxxxxx) 정상 처리, SRAM 크기 20KB로 수정, systickFrequency 8MHz로 낮춰 실시간 타이밍 보정 - stm32f1_rcc.py(~/.local): UTF-8 인코딩 선언, PLLRDY가 PLLON을 동적으로 추적해 SystemClock_Config() HAL 폴링 루프 해소 - routes-arduino.ts: SystemClock_Config() 스케치에서 제거(bit-band 우회), 보드 선택 6종 whitelist, Companion WS 재연결 스팸 수정 - arduino-emulator.html + code.js: Renode 세션 라우팅 + 보라 배지 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3019,6 +3019,77 @@ function initEsp32Mode(sessionId, fname, fqbn) {
|
||||
_adcShowSection();
|
||||
}
|
||||
|
||||
// ── STM32 Renode mode ─────────────────────────────────────────────────────────
|
||||
let _renodeSessionId = null;
|
||||
let _renodeEvtSource = null;
|
||||
|
||||
function initSTM32RenodeMode(sessionId, fname, fqbn) {
|
||||
_renodeSessionId = sessionId;
|
||||
document.querySelector('.header h1').textContent = '🖥️ STM32 Emulator (Renode)';
|
||||
document.getElementById('filename').textContent = fname;
|
||||
|
||||
document.getElementById('chip-badge')?.remove();
|
||||
const badge = document.createElement('span');
|
||||
badge.id = 'chip-badge';
|
||||
badge.style.cssText = 'display:inline-block;padding:2px 7px;border-radius:4px;font-size:10px;font-weight:600;border:1px solid;flex-shrink:0;';
|
||||
const family = fqbn.includes('GenF4') || fqbn.includes('f4') ? 'STM32F4' : 'STM32F1';
|
||||
badge.textContent = family + ' · Renode';
|
||||
badge.style.background = '#1a0a3a'; badge.style.color = '#c084fc'; badge.style.borderColor = '#7c3aed';
|
||||
document.getElementById('filename').after(badge);
|
||||
|
||||
document.querySelector('.board-svg')?.style && (document.querySelector('.board-svg').style.display = 'none');
|
||||
document.querySelector('.board-title')?.style && (document.querySelector('.board-title').style.display = 'none');
|
||||
document.getElementById('btn-stop')?.style && (document.getElementById('btn-stop').style.display = 'none');
|
||||
document.getElementById('btn-reset')?.style && (document.getElementById('btn-reset').style.display = 'none');
|
||||
|
||||
document.getElementById('renode-stop-btn')?.remove();
|
||||
const toolbar = document.querySelector('.toolbar-right') || document.querySelector('.toolbar');
|
||||
if (toolbar) {
|
||||
const stopBtn = document.createElement('button');
|
||||
stopBtn.id = 'renode-stop-btn'; stopBtn.textContent = '■ 정지';
|
||||
stopBtn.style.cssText = 'font-size:12px;padding:3px 8px;background:#3a1010;border:1px solid #cc4444;color:#ff8888;border-radius:4px;cursor:pointer';
|
||||
stopBtn.onclick = () => {
|
||||
fetch('/api/arduino/renode/' + _renodeSessionId, { method: 'DELETE', credentials: 'include', keepalive: true }).catch(() => {});
|
||||
if (_renodeEvtSource) { _renodeEvtSource.close(); _renodeEvtSource = null; }
|
||||
setStatus('off', '정지됨');
|
||||
};
|
||||
toolbar.append(stopBtn);
|
||||
}
|
||||
|
||||
const serialInput = document.getElementById('serial-input');
|
||||
const serialSend = document.querySelector('.serial-send');
|
||||
if (serialInput) serialInput.disabled = false;
|
||||
if (serialSend) serialSend.removeAttribute('disabled');
|
||||
const doSend = () => {
|
||||
const text = serialInput?.value || '';
|
||||
if (!text) return;
|
||||
fetch('/api/arduino/renode/' + _renodeSessionId + '/input', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: text + '\n' }),
|
||||
}).catch(() => {});
|
||||
if (serialInput) serialInput.value = '';
|
||||
};
|
||||
serialInput?.addEventListener('keydown', e => { if (e.key === 'Enter') doSend(); });
|
||||
serialSend?.addEventListener('click', doSend);
|
||||
|
||||
setStatus('loading', 'Renode 기동 중...');
|
||||
_renodeEvtSource = new EventSource('/api/arduino/renode/' + sessionId + '/output', { withCredentials: true });
|
||||
_renodeEvtSource.onopen = () => setStatus('running', '실행 중');
|
||||
_renodeEvtSource.onerror = () => setStatus('error', '연결 오류');
|
||||
_renodeEvtSource.onmessage = e => {
|
||||
try {
|
||||
const d = JSON.parse(e.data);
|
||||
if (d.serial) appendSerial(d.serial);
|
||||
if (d.exit !== undefined) setStatus('off', '종료');
|
||||
} catch {}
|
||||
};
|
||||
window.addEventListener('beforeunload', () => {
|
||||
fetch('/api/arduino/renode/' + sessionId, { method: 'DELETE', credentials: 'include', keepalive: true }).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function _buildEsp32PinPopup() {
|
||||
const body = document.getElementById('pin-popup-body');
|
||||
if (!body) return;
|
||||
@@ -4231,6 +4302,10 @@ async function boot() {
|
||||
const savedSketch = localStorage.getItem('esp8266_sketch');
|
||||
if (savedSketch) { _esp8266SketchCode = savedSketch; localStorage.removeItem('esp8266_sketch'); }
|
||||
initEsp8266Mode(qemuSession, fname, fqbn);
|
||||
} else if (qemuSession.startsWith('renode_')) {
|
||||
const savedSketch = localStorage.getItem('stm32_sketch');
|
||||
if (savedSketch) { localStorage.removeItem('stm32_sketch'); }
|
||||
initSTM32RenodeMode(qemuSession, fname, fqbn);
|
||||
} else {
|
||||
// Restore sketch code from code editor (localStorage: shared across windows)
|
||||
const savedSketch = localStorage.getItem('esp32_sketch');
|
||||
|
||||
+37
-6
@@ -610,6 +610,7 @@ function _termOnRunSuccess(shellId) {
|
||||
// ── Companion WebSocket (local Python execution) ───────────────────────────
|
||||
let companionWs = null;
|
||||
let companionConnected = false;
|
||||
let companionEverConnected = false; // 한 번이라도 성공한 적 있는지
|
||||
const COMPANION_URL = 'ws://127.0.0.1:9000';
|
||||
let companionPlatform = 'unix'; // 'windows' | 'unix', set on shell_ready
|
||||
let companionHome = ''; // home directory from companion
|
||||
@@ -618,11 +619,12 @@ const localShellIds = new Set(); // shell IDs routed to companion PTY (tab id =
|
||||
function companionConnect() {
|
||||
if (companionWs && (companionWs.readyState === WebSocket.OPEN || companionWs.readyState === WebSocket.CONNECTING)) return;
|
||||
try { companionWs = new WebSocket(COMPANION_URL); } catch { return; }
|
||||
companionWs.onopen = () => { companionConnected = true; updateCompanionStatus(true); };
|
||||
companionWs.onopen = () => { companionConnected = true; companionEverConnected = true; updateCompanionStatus(true); };
|
||||
companionWs.onclose = () => {
|
||||
companionConnected = false; companionWs = null;
|
||||
updateCompanionStatus(false);
|
||||
setTimeout(companionConnect, 5000);
|
||||
// 한 번이라도 연결됐던 경우에만 자동 재연결 (미연결 상태서 콘솔 에러 spam 방지)
|
||||
if (companionEverConnected) setTimeout(companionConnect, 5000);
|
||||
};
|
||||
companionWs.onerror = () => { companionConnected = false; };
|
||||
companionWs.onmessage = (ev) => {
|
||||
@@ -1009,6 +1011,29 @@ function openArduinoEmulator() {
|
||||
toggleEmuPanel();
|
||||
});
|
||||
|
||||
} else if (fqbn.startsWith('STMicroelectronics:stm32')) {
|
||||
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
|
||||
_emuPanelLoad('/arduino-emulator.html?loading=1', `STM32 — ${file.name}`);
|
||||
codeAiAppend('system', `🖥️ STM32 Renode 에뮬레이터 컴파일 중... [${_csbSelBoard.name}]`);
|
||||
try { localStorage.setItem('stm32_sketch', content); } catch {}
|
||||
api('/api/arduino/renode/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ code: content, fqbn }) })
|
||||
.then(d => {
|
||||
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
|
||||
if (d.error) {
|
||||
sendCompileErrorToAI(d.error, file.name, content);
|
||||
toggleEmuPanel();
|
||||
return;
|
||||
}
|
||||
const _proj = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
|
||||
const url = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
|
||||
_emuPanelLoad(url, `STM32 — ${file.name}`);
|
||||
codeAiAppend('system', `✅ STM32 Renode 에뮬레이터 실행 중.`);
|
||||
}).catch(e => {
|
||||
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
|
||||
codeAiAppend('system', '⚠️ ' + e.message);
|
||||
toggleEmuPanel();
|
||||
});
|
||||
|
||||
} else {
|
||||
// AVR 및 기타
|
||||
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
|
||||
@@ -1264,7 +1289,7 @@ const CODE_RUN_MODES = {
|
||||
{ id: 'terminal', icon: '⌨', label: 'bash', desc: '터미널에서 실행' },
|
||||
],
|
||||
ino: [
|
||||
{ id: 'arduino', icon: '🔌', label: 'Arduino 에뮬레이터', desc: 'AVR 브라우저 에뮬레이터 (avr8js)' },
|
||||
{ id: 'arduino', icon: '🔌', label: '에뮬레이터', desc: 'AVR/STM32/ESP 에뮬레이터 (보드에 따라 자동 선택)' },
|
||||
{ id: 'esp32', icon: '⚡', label: 'ESP32 플래시', desc: 'Web Serial로 ESP32에 업로드' },
|
||||
{ id: 'terminal', icon: '⌨', label: 'arduino-cli', desc: '터미널에서 빌드/업로드' },
|
||||
],
|
||||
@@ -1485,8 +1510,9 @@ function codeRunWith(mode) {
|
||||
if (mode === 'arduino') {
|
||||
const _isEsp32Board = (_csbSelBoard.fqbn.split(':')[1] || '') === 'esp32';
|
||||
const _isEsp8266Board = (_csbSelBoard.fqbn.split(':')[0] || '') === 'esp8266';
|
||||
if (_isEsp32Board || _isEsp8266Board) {
|
||||
// ESP32/ESP8266 계열 — 팝업 차단 방지를 위해 openArduinoEmulator()와 같은 경로 사용
|
||||
const _isStm32Board = _csbSelBoard.fqbn.startsWith('STMicroelectronics:stm32');
|
||||
if (_isEsp32Board || _isEsp8266Board || _isStm32Board) {
|
||||
// ESP32/ESP8266/STM32 계열 — openArduinoEmulator()로 에뮬레이터 실행
|
||||
openArduinoEmulator(); return;
|
||||
}
|
||||
codeAiAppend('system', `🔌 컴파일 중... (${file.name}) [${_csbSelBoard.name}]`);
|
||||
@@ -4753,6 +4779,9 @@ function _csbBoardSelSave(board) {
|
||||
if (board.qemu === 'esp8266-native') {
|
||||
badge.textContent = '🖥 Native'; badge.style.display = 'inline';
|
||||
badge.style.background = '#0c3a4a'; badge.style.color = '#67e8f9'; badge.style.borderColor = '#0e7490';
|
||||
} else if (board.qemu === 'renode') {
|
||||
badge.textContent = '🖥 Renode'; badge.style.display = 'inline';
|
||||
badge.style.background = '#1a0a3a'; badge.style.color = '#c084fc'; badge.style.borderColor = '#7c3aed';
|
||||
} else if (board.qemu) {
|
||||
badge.textContent = '🖥 QEMU'; badge.style.display = 'inline';
|
||||
badge.style.background = board.qemu === 'esp32s3' ? '#1a3a5a' : '#1a3a1a';
|
||||
@@ -4794,7 +4823,9 @@ async function csbBoardSelSearch() {
|
||||
const sel = b.fqbn === _csbSelBoard.fqbn;
|
||||
const qemuBadge = b.qemu === 'esp8266-native'
|
||||
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;background:#0c3a4a;color:#67e8f9;border:1px solid #0e7490">🖥 Native</span>`
|
||||
: b.qemu
|
||||
: b.qemu === 'renode'
|
||||
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;background:#1a0a3a;color:#c084fc;border:1px solid #7c3aed">🖥 Renode</span>`
|
||||
: b.qemu
|
||||
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;
|
||||
background:${b.qemu==='esp32s3'?'#1a3a5a':'#1a3a1a'};
|
||||
color:${b.qemu==='esp32s3'?'#60a5fa':'#22c55e'};
|
||||
|
||||
Reference in New Issue
Block a user