feat: STM32 Renode SSD1306 OLED 지원

- mocks-stm32.ts: _RN_SSD1306_MOCK_H 추가
  - 5×7 소프트웨어 폰트 포함 (96 ASCII)
  - 내부 framebuffer + display() → L:oD: 텔레메트리
  - Wire stub, GFX base class (_RN_GFX_DEFINED 가드)
- routes-arduino.ts: SSD1306 감지 → mock 주입
- arduino-emulator.html: L:O: (초기화) + L:oD: (프레임) 핸들러

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-30 20:30:25 +09:00
co-authored by Claude Sonnet 4.6
parent f0270be9fe
commit 26f928a873
3 changed files with 292 additions and 8 deletions
+51 -7
View File
@@ -3125,7 +3125,51 @@ function _stm32ParseGpio(raw) {
return out;
}
let _stm32OledInited = false;
let _stm32OledW = 128, _stm32OledH = 64;
function _stm32OledFlush(hexStr, inv) {
const c = document.getElementById('oled-canvas');
if (!c) return;
const ctx = c.getContext('2d');
const pages = _stm32OledH / 8;
const img = ctx.createImageData(c.width, c.height);
const sx = c.width / _stm32OledW, sy = c.height / _stm32OledH;
for (let page = 0; page < pages; page++) {
for (let col = 0; col < _stm32OledW; col++) {
const idx = (page * _stm32OledW + col) * 2;
const byte = parseInt(hexStr.slice(idx, idx + 2), 16) || 0;
for (let bit = 0; bit < 8; bit++) {
const on = !!((byte >> bit) & 1) !== inv;
const row = page * 8 + bit;
const r = on ? 0x60 : 0, g = on ? 0xa5 : 0, b = on ? 0xfa : 0;
for (let dy = 0; dy < sy; dy++) for (let dx = 0; dx < sx; dx++) {
const pi = ((Math.floor(row * sy) + dy) * c.width + Math.floor(col * sx) + dx) * 4;
img.data[pi] = r; img.data[pi+1] = g; img.data[pi+2] = b; img.data[pi+3] = 255;
}
}
}
}
ctx.putImageData(img, 0, 0);
oledSetActive(true);
}
function _stm32LcdDraw(op, data) {
// SSD1306 OLED 초기화
if (op === 'O') {
const [w, h] = data.split(',').map(Number);
_stm32OledW = w || 128; _stm32OledH = h || 64;
_stm32OledInited = true;
const c = document.getElementById('oled-canvas');
if (c) { c.width = _stm32OledW * 2; c.height = _stm32OledH * 2; }
oledSetActive(true);
return;
}
// SSD1306 프레임 데이터 (대문자=정상, 소문자=반전)
if (op === 'oD' || op === 'od') {
_stm32OledFlush(data, op === 'od');
return;
}
if (op === 'I') {
const [w, h] = data.split(',').map(Number);
_esp32TftW = w || 240; _esp32TftH = h || 320; _esp32TftCtx2d = null;
@@ -3141,13 +3185,13 @@ function _stm32LcdDraw(op, data) {
}
const n = data.split(',').map(Number);
switch (op) {
case 'r': _esp32TftDraw('B:' + n.join(',')); break; // fillRect x,y,w,h,c
case 'h': _esp32TftDraw('B:' + [n[0],n[1],n[2],1,n[3]].join(',')); break; // hline → fillRect h=1
case 'v': _esp32TftDraw('B:' + [n[0],n[1],1,n[2],n[3]].join(',')); break; // vline → fillRect w=1
case 'p': _esp32TftDraw('P:' + n.join(',')); break; // pixel x,y,c
case 'l': _esp32TftDraw('L:' + n.join(',')); break; // line x0,y0,x1,y1,c
case 'c': _esp32TftDraw('C:' + [n[0],n[1],n[2],n[4],n[3]].join(',')); break; // circle cx,cy,r,fill,c → C:cx,cy,r,c,fill
case 't': _esp32TftDraw('T:' + n.join(',')); break; // char x,y,fg,bg,size,charcode
case 'r': _esp32TftDraw('B:' + n.join(',')); break;
case 'h': _esp32TftDraw('B:' + [n[0],n[1],n[2],1,n[3]].join(',')); break;
case 'v': _esp32TftDraw('B:' + [n[0],n[1],1,n[2],n[3]].join(',')); break;
case 'p': _esp32TftDraw('P:' + n.join(',')); break;
case 'l': _esp32TftDraw('L:' + n.join(',')); break;
case 'c': _esp32TftDraw('C:' + [n[0],n[1],n[2],n[4],n[3]].join(',')); break;
case 't': _esp32TftDraw('T:' + n.join(',')); break;
}
}