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
+214
View File
@@ -60,6 +60,7 @@ static inline void _rn_lt5(char op, int16_t a, int16_t b, int16_t c, int16_t d,
/* ══════════════════════════════════════════════════════ /* ══════════════════════════════════════════════════════
Adafruit_GFX — complete replacement, no SPI Adafruit_GFX — complete replacement, no SPI
══════════════════════════════════════════════════════ */ ══════════════════════════════════════════════════════ */
#define _RN_GFX_DEFINED
class Adafruit_GFX : public Print { class Adafruit_GFX : public Print {
protected: protected:
int16_t _width, _height; int16_t _width, _height;
@@ -184,3 +185,216 @@ public:
#endif /* _RN_ILI9341_MOCK_H */ #endif /* _RN_ILI9341_MOCK_H */
`; `;
export const _RN_SSD1306_MOCK_H = `#pragma once
#ifndef _RN_SSD1306_MOCK_H
#define _RN_SSD1306_MOCK_H
#include <Arduino.h>
#include <string.h>
#define SSD1306_SWITCHCAPVCC 0x02
#define SSD1306_EXTERNALVCC 0x01
#define SSD1306_BLACK 0
#define SSD1306_WHITE 1
#define SSD1306_INVERSE 2
#ifndef BLACK
#define BLACK SSD1306_BLACK
#define WHITE SSD1306_WHITE
#define INVERSE SSD1306_INVERSE
#endif
/* ── Wire stub (SSD1306 mock이 실제로 I2C를 사용하지 않음) ── */
#ifndef _RN_WIRE_STUB
#define _RN_WIRE_STUB
class TwoWire {
public:
void begin() {} void begin(uint8_t,uint8_t) {} void setClock(uint32_t) {}
void beginTransmission(uint8_t) {} uint8_t endTransmission(bool=true){return 0;}
uint8_t requestFrom(uint8_t,uint8_t){return 0;}
size_t write(uint8_t){return 1;} size_t write(const uint8_t*,size_t n){return n;}
int available(){return 0;} int read(){return -1;} int peek(){return -1;}
};
static TwoWire Wire;
#endif
/* ── Minimal GFX base (ILI9341 mock가 없을 때만 정의) ── */
#ifndef _RN_GFX_DEFINED
#define _RN_GFX_DEFINED
struct GFXfont { const uint8_t* bitmap; const void* glyph; uint8_t first,last,yAdvance; };
static const uint8_t _SSD_FONT5X7[96][5] = {
{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},
{0x14,0x7F,0x14,0x7F,0x14},{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},
{0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},{0x00,0x1C,0x22,0x41,0x00},
{0x00,0x41,0x22,0x1C,0x00},{0x14,0x08,0x3E,0x08,0x14},{0x08,0x08,0x3E,0x08,0x08},
{0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},
{0x20,0x10,0x08,0x04,0x02},{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},
{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},{0x18,0x14,0x12,0x7F,0x10},
{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},
{0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00},
{0x00,0x56,0x36,0x00,0x00},{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},
{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},{0x32,0x49,0x79,0x41,0x3E},
{0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
{0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},
{0x3E,0x41,0x49,0x49,0x7A},{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},
{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},{0x7F,0x40,0x40,0x40,0x40},
{0x7F,0x02,0x0C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},
{0x46,0x49,0x49,0x49,0x31},{0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},
{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},{0x63,0x14,0x08,0x14,0x63},
{0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43},{0x00,0x7F,0x41,0x41,0x00},
{0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04},
{0x40,0x40,0x40,0x40,0x40},{0x00,0x01,0x02,0x04,0x00},{0x20,0x54,0x54,0x54,0x78},
{0x7F,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},{0x38,0x44,0x44,0x48,0x7F},
{0x38,0x54,0x54,0x54,0x18},{0x08,0x7E,0x09,0x01,0x02},{0x0C,0x52,0x52,0x52,0x3E},
{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x44,0x3D,0x00},
{0x7F,0x10,0x28,0x44,0x00},{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x18,0x04,0x78},
{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},{0x7C,0x14,0x14,0x14,0x08},
{0x08,0x14,0x14,0x18,0x7C},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20},
{0x04,0x3F,0x44,0x40,0x20},{0x3C,0x40,0x40,0x40,0x3C},{0x1C,0x20,0x40,0x20,0x1C},
{0x3C,0x40,0x30,0x40,0x3C},{0x44,0x28,0x10,0x28,0x44},{0x0C,0x50,0x50,0x50,0x3C},
{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},{0x00,0x00,0x7F,0x00,0x00},
{0x00,0x41,0x36,0x08,0x00},{0x10,0x08,0x08,0x10,0x08},{0x3C,0x26,0x23,0x26,0x3C},
};
class Print {
public:
virtual size_t write(uint8_t) = 0;
size_t print(const char* s){ size_t n=0; if(s) while(*s) n+=write((uint8_t)*s++); return n; }
size_t println(const char* s){ return print(s)+write('\\n'); }
size_t print(int v, int=10){ char b[16]; snprintf(b,sizeof(b),"%d",v); return print(b); }
size_t println(int v, int=10){ return print(v)+write('\\n'); }
size_t print(long v, int=10){ char b[24]; snprintf(b,sizeof(b),"%ld",v); return print(b); }
size_t println(long v, int=10){ return print(v)+write('\\n'); }
size_t print(unsigned v, int=10){ char b[16]; snprintf(b,sizeof(b),"%u",v); return print(b); }
size_t print(float v, int d=2){ char b[24]; char fmt[8]; snprintf(fmt,sizeof(fmt),"%%.%df",d); snprintf(b,sizeof(b),fmt,v); return print(b); }
size_t println(float v, int d=2){ return print(v,d)+write('\\n'); }
size_t println(){ return write('\\n'); }
};
class Adafruit_GFX : public Print {
protected:
int16_t _width, _height, _cx=0, _cy=0;
uint16_t _fg=1, _bg=0;
uint8_t _ts=1;
public:
Adafruit_GFX(int16_t w,int16_t h):_width(w),_height(h){}
virtual void drawPixel(int16_t x,int16_t y,uint16_t c)=0;
int16_t width()const{return _width;} int16_t height()const{return _height;}
void setTextColor(uint16_t c){_fg=c;} void setTextColor(uint16_t f,uint16_t b){_fg=f;_bg=b;}
void setTextSize(uint8_t s){_ts=s?s:1;} void setCursor(int16_t x,int16_t y){_cx=x;_cy=y;}
void setFont(const GFXfont*){} void setTextWrap(bool){}
int16_t getCursorX()const{return _cx;} int16_t getCursorY()const{return _cy;}
void fillScreen(uint16_t c){fillRect(0,0,_width,_height,c);}
void fillRect(int16_t x,int16_t y,int16_t w,int16_t h,uint16_t c){
for(int j=y;j<y+h;j++) for(int i=x;i<x+w;i++) drawPixel(i,j,c);
}
void drawRect(int16_t x,int16_t y,int16_t w,int16_t h,uint16_t c){
for(int i=x;i<x+w;i++){drawPixel(i,y,c);drawPixel(i,y+h-1,c);}
for(int j=y+1;j<y+h-1;j++){drawPixel(x,j,c);drawPixel(x+w-1,j,c);}
}
void drawFastHLine(int16_t x,int16_t y,int16_t w,uint16_t c){fillRect(x,y,w,1,c);}
void drawFastVLine(int16_t x,int16_t y,int16_t h,uint16_t c){fillRect(x,y,1,h,c);}
void drawLine(int16_t x0,int16_t y0,int16_t x1,int16_t y1,uint16_t c){
int dx=abs(x1-x0),dy=-abs(y1-y0),sx=x0<x1?1:-1,sy=y0<y1?1:-1,e=dx+dy,e2;
while(true){drawPixel(x0,y0,c);if(x0==x1&&y0==y1)break;e2=2*e;if(e2>=dy){e+=dy;x0+=sx;}if(e2<=dx){e+=dx;y0+=sy;}}
}
void drawCircle(int16_t cx,int16_t cy,int16_t r,uint16_t c){
int x=0,y=r,d=3-2*r;
while(y>=x){
drawPixel(cx+x,cy+y,c);drawPixel(cx-x,cy+y,c);drawPixel(cx+x,cy-y,c);drawPixel(cx-x,cy-y,c);
drawPixel(cx+y,cy+x,c);drawPixel(cx-y,cy+x,c);drawPixel(cx+y,cy-x,c);drawPixel(cx-y,cy-x,c);
d<0?d+=4*x+6:(d+=4*(x-y)+10,y--);x++;
}
}
void fillCircle(int16_t cx,int16_t cy,int16_t r,uint16_t c){
for(int y=-r;y<=r;y++) for(int x=-r;x<=r;x++) if(x*x+y*y<=r*r) drawPixel(cx+x,cy+y,c);
}
void drawRoundRect(int16_t x,int16_t y,int16_t w,int16_t h,int16_t,uint16_t c){drawRect(x,y,w,h,c);}
void fillRoundRect(int16_t x,int16_t y,int16_t w,int16_t h,int16_t,uint16_t c){fillRect(x,y,w,h,c);}
void drawTriangle(int16_t x0,int16_t y0,int16_t x1,int16_t y1,int16_t x2,int16_t y2,uint16_t c){
drawLine(x0,y0,x1,y1,c);drawLine(x1,y1,x2,y2,c);drawLine(x2,y2,x0,y0,c);
}
void fillTriangle(int16_t x0,int16_t y0,int16_t x1,int16_t y1,int16_t x2,int16_t y2,uint16_t c){
drawTriangle(x0,y0,x1,y1,x2,y2,c);
}
static uint16_t color565(uint8_t r,uint8_t g,uint8_t b){
return ((uint16_t)(r&0xF8)<<8)|((uint16_t)(g&0xFC)<<3)|(b>>3);
}
size_t write(uint8_t ch) override {
if(ch=='\\n'){_cy+=8*_ts;_cx=0;return 1;}
if(ch=='\\r')return 1;
if(ch<0x20||ch>0x7F)ch='?';
const uint8_t* col=_SSD_FONT5X7[ch-0x20];
for(int c=0;c<5;c++){
uint8_t line=col[c];
for(int r=0;r<8;r++,line>>=1)
drawPixel(_cx+c*_ts,_cy+r*_ts,(line&1)?_fg:_bg);
}
_cx+=6*_ts;
return 1;
}
};
#endif /* _RN_GFX_DEFINED */
/* ══════════════════════════════════════════════════════
Adafruit_SSD1306 — framebuffer + UART telemetry
══════════════════════════════════════════════════════ */
class Adafruit_SSD1306 : public Adafruit_GFX {
uint8_t _buf[1024]; // 128×64 / 8 pages
bool _inited = false;
bool _inv = false;
public:
// I2C constructor
Adafruit_SSD1306(uint8_t w,uint8_t h,TwoWire* =nullptr,int8_t=-1)
: Adafruit_GFX(w,h){ memset(_buf,0,sizeof(_buf)); }
// SPI constructor
Adafruit_SSD1306(uint8_t w,uint8_t h,int8_t,int8_t,int8_t,int8_t,int8_t=-1)
: Adafruit_GFX(w,h){ memset(_buf,0,sizeof(_buf)); }
bool begin(uint8_t=SSD1306_SWITCHCAPVCC,uint8_t=0x3C,bool=true,bool=true){
_inited=true;
char b[16]; snprintf(b,sizeof(b),"L:O:%d,%d",(int)_width,(int)_height);
Serial.write((uint8_t)2); Serial.print(b); Serial.write((uint8_t)3);
return true;
}
void clearDisplay(){ memset(_buf,0,sizeof(_buf)); }
void drawPixel(int16_t x,int16_t y,uint16_t color) override {
if(x<0||x>=_width||y<0||y>=_height) return;
const int page=y/8, bit=y&7;
if(color) _buf[x+page*_width]|= (1<<bit);
else _buf[x+page*_width]&=~(1<<bit);
}
void display(){
if(!_inited) return;
static const char HEX[]="0123456789ABCDEF";
const int pages=_height/8;
Serial.write((uint8_t)2);
Serial.print(_inv?"L:od:":"L:oD:");
for(int i=0;i<_width*pages;i++){
Serial.write((uint8_t)HEX[_buf[i]>>4]);
Serial.write((uint8_t)HEX[_buf[i]&0xF]);
}
Serial.write((uint8_t)3);
}
void invertDisplay(bool i){ _inv=i; }
void dim(bool){}
void startScrollRight(uint8_t,uint8_t){}
void startScrollLeft(uint8_t,uint8_t){}
void startScrollDiagRight(uint8_t,uint8_t){}
void startScrollDiagLeft(uint8_t,uint8_t){}
void stopScroll(){}
void ssd1306_command(uint8_t){}
bool getPixel(int16_t x,int16_t y)const{
if(x<0||x>=_width||y<0||y>=_height) return false;
return (_buf[x+(y/8)*_width]>>(y&7))&1;
}
};
#endif /* _RN_SSD1306_MOCK_H */
`;
+27 -1
View File
@@ -12,7 +12,7 @@ import {
_WIRE_MOCK_H,_MPU6050_MOCK_H,_BME280_MOCK_H,_ONEWIRE_MOCK_H,_DALLASTEMP_MOCK_H, _WIRE_MOCK_H,_MPU6050_MOCK_H,_BME280_MOCK_H,_ONEWIRE_MOCK_H,_DALLASTEMP_MOCK_H,
_RELAY_MOCK_H,_L298N_MOCK_H,_SPI_MOCK_H,_TFT_MOCK_H, _RELAY_MOCK_H,_L298N_MOCK_H,_SPI_MOCK_H,_TFT_MOCK_H,
} from './mocks-esp32'; } from './mocks-esp32';
import { _RN_ILI9341_MOCK_H } from './mocks-stm32'; import { _RN_ILI9341_MOCK_H, _RN_SSD1306_MOCK_H } from './mocks-stm32';
export function registerArduinoRoutes(app: express.Application, getSessionUser: (req: express.Request) => any): void { export function registerArduinoRoutes(app: express.Application, getSessionUser: (req: express.Request) => any): void {
@@ -2013,6 +2013,32 @@ app.post('/api/arduino/renode/start', async (req, res) => {
.replace(/&(SPI\d+)\b/g, 'nullptr'); .replace(/&(SPI\d+)\b/g, 'nullptr');
} }
// SSD1306 OLED 사용 시: 실제 라이브러리를 로컬 mock으로 교체
const _hasSSD1306 = /Adafruit_SSD1306\.h/.test(patchedCode);
if (_hasSSD1306) {
const fs3 = require('fs');
fs3.writeFileSync(sketchDir + '/Adafruit_SSD1306.h', _RN_SSD1306_MOCK_H);
if (!_hasILI9341) {
// ILI9341 mock가 없으면 GFX 헤더도 SSD1306 mock에서 제공
fs3.writeFileSync(sketchDir + '/Adafruit_GFX.h', '#pragma once\n#include "Adafruit_SSD1306.h"\n');
patchedCode = patchedCode.replace(/#include\s*<Adafruit_GFX\.h>/g, '#include "Adafruit_GFX.h"');
}
// Wire.h stub (SSD1306 I2C init 컴파일 통과용)
if (!fs3.existsSync(sketchDir + '/Wire.h')) {
fs3.writeFileSync(sketchDir + '/Wire.h',
'#pragma once\nclass TwoWire{\npublic:\n void begin(){} void begin(uint8_t,uint8_t){} void setClock(uint32_t){}\n' +
' void beginTransmission(uint8_t){} uint8_t endTransmission(bool=true){return 0;}\n' +
' uint8_t requestFrom(uint8_t,uint8_t){return 0;}\n' +
' size_t write(uint8_t){return 1;} size_t write(const uint8_t*,size_t n){return n;}\n' +
' int available(){return 0;} int read(){return -1;} int peek(){return -1;}\n};\n' +
'static TwoWire Wire;\n'
);
patchedCode = patchedCode.replace(/#include\s*<Wire\.h>/g, '#include "Wire.h"');
}
patchedCode = patchedCode
.replace(/#include\s*<Adafruit_SSD1306\.h>/g, '#include "Adafruit_SSD1306.h"');
}
require('fs').writeFileSync(sketchDir + '/sketch.ino', patchedCode); require('fs').writeFileSync(sketchDir + '/sketch.ino', patchedCode);
const execQ = (cmd: string, ms = 60000): Promise<{ stdout: string; stderr: string }> => const execQ = (cmd: string, ms = 60000): Promise<{ stdout: string; stderr: string }> =>
+51 -7
View File
@@ -3125,7 +3125,51 @@ function _stm32ParseGpio(raw) {
return out; 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) { 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') { if (op === 'I') {
const [w, h] = data.split(',').map(Number); const [w, h] = data.split(',').map(Number);
_esp32TftW = w || 240; _esp32TftH = h || 320; _esp32TftCtx2d = null; _esp32TftW = w || 240; _esp32TftH = h || 320; _esp32TftCtx2d = null;
@@ -3141,13 +3185,13 @@ function _stm32LcdDraw(op, data) {
} }
const n = data.split(',').map(Number); const n = data.split(',').map(Number);
switch (op) { switch (op) {
case 'r': _esp32TftDraw('B:' + n.join(',')); break; // fillRect x,y,w,h,c case 'r': _esp32TftDraw('B:' + n.join(',')); break;
case 'h': _esp32TftDraw('B:' + [n[0],n[1],n[2],1,n[3]].join(',')); break; // hline → fillRect h=1 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; // vline → fillRect w=1 case 'v': _esp32TftDraw('B:' + [n[0],n[1],1,n[2],n[3]].join(',')); break;
case 'p': _esp32TftDraw('P:' + n.join(',')); break; // pixel x,y,c case 'p': _esp32TftDraw('P:' + n.join(',')); break;
case 'l': _esp32TftDraw('L:' + n.join(',')); break; // line x0,y0,x1,y1,c case 'l': _esp32TftDraw('L:' + n.join(',')); break;
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 'c': _esp32TftDraw('C:' + [n[0],n[1],n[2],n[4],n[3]].join(',')); break;
case 't': _esp32TftDraw('T:' + n.join(',')); break; // char x,y,fg,bg,size,charcode case 't': _esp32TftDraw('T:' + n.join(',')); break;
} }
} }