feat: STM32 Renode 시리얼 출력 수정 + 로그 정리

- uartBuf 버퍼 추가: SSE 연결 전 도착한 UART 데이터 재전송 (타이밍 이슈 해결)
- Renode 로그 라인버퍼링 + ANSI 코드 제거
- 시리얼 모니터에 WARNING/INFO 숨김, Machine started + ERROR만 표시
- ILI9341 mock: &SPI1 → nullptr 패치 (cast-macro address-of 오류 수정)
- STM32 보드 목록: 블루필/블랙필 한국어 표기 + Nucleo 3종 유지

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-30 13:52:12 +09:00
co-authored by Claude Sonnet 4.6
parent e520d09ca1
commit 5e9596e188
3 changed files with 302 additions and 19 deletions
+186
View File
@@ -0,0 +1,186 @@
// Arduino emulator mock headers — STM32 Renode
// ILI9341/GFX를 완전히 대체하는 UART 텔레메트리 전용 구현
// 프로토콜: \x02 L:op:data \x03 (STX / ETX binary framing)
export const _RN_ILI9341_MOCK_H = `#pragma once
#ifndef _RN_ILI9341_MOCK_H
#define _RN_ILI9341_MOCK_H
#include <Arduino.h>
/* ── color constants ── */
#define ILI9341_BLACK 0x0000
#define ILI9341_WHITE 0xFFFF
#define ILI9341_RED 0xF800
#define ILI9341_GREEN 0x07E0
#define ILI9341_BLUE 0x001F
#define ILI9341_CYAN 0x07FF
#define ILI9341_MAGENTA 0xF81F
#define ILI9341_YELLOW 0xFFE0
#define ILI9341_ORANGE 0xFD20
#define ILI9341_DARKGREY 0x7BEF
#define ILI9341_LIGHTGREY 0xC618
#define ILI9341_NAVY 0x000F
#define ILI9341_DARKGREEN 0x03E0
#define ILI9341_DARKCYAN 0x03EF
#define ILI9341_MAROON 0x7800
#define ILI9341_PURPLE 0x780F
#define ILI9341_OLIVE 0x7BE0
#define ILI9341_PINK 0xFC18
#define ILI9341_TFTWIDTH 240
#define ILI9341_TFTHEIGHT 320
/* ── text datum (TFT_eSPI compat) ── */
#ifndef TL_DATUM
#define TL_DATUM 0
#define TC_DATUM 1
#define TR_DATUM 2
#define ML_DATUM 3
#define MC_DATUM 4
#define MR_DATUM 5
#define BL_DATUM 6
#define BC_DATUM 7
#define BR_DATUM 8
#endif
/* ── GFX font stub ── */
struct GFXfont {
const uint8_t* bitmap; const void* glyph;
uint8_t first, last, yAdvance;
};
/* ── internal telemetry helper ── */
static inline void _rn_lt5(char op, int16_t a, int16_t b, int16_t c, int16_t d, uint16_t e) {
char buf[52];
snprintf(buf, sizeof(buf), "L:%c:%d,%d,%d,%d,%d", op, (int)a, (int)b, (int)c, (int)d, (int)e);
Serial.write((uint8_t)2);
Serial.print(buf);
Serial.write((uint8_t)3);
}
/* ══════════════════════════════════════════════════════
Adafruit_GFX — complete replacement, no SPI
══════════════════════════════════════════════════════ */
class Adafruit_GFX : public Print {
protected:
int16_t _width, _height;
int16_t _cx = 0, _cy = 0;
uint16_t _fg = 0xFFFF, _bg = 0x0000;
uint8_t _ts = 1;
uint8_t _rotation = 0;
const GFXfont* _gfxFont = nullptr;
public:
Adafruit_GFX(int16_t w = 240, int16_t h = 320) : _width(w), _height(h) {}
int16_t width() const { return _width; }
int16_t height() const { return _height; }
uint8_t getRotation() const { return _rotation; }
/* ── core drawing (virtual so subclasses can override) ── */
virtual void drawPixel(int16_t x, int16_t y, uint16_t c)
{ _rn_lt5('p', x, y, 1, 1, c); }
virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c)
{ _rn_lt5('r', x, y, w, h, c); }
virtual void fillScreen(uint16_t c)
{ _rn_lt5('r', 0, 0, _width, _height, c); }
virtual void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t c)
{ _rn_lt5('h', x, y, w, 1, c); }
virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t c)
{ _rn_lt5('v', x, y, 1, h, c); }
virtual void setRotation(uint8_t r) {
_rotation = r & 3;
if ((_rotation == 1 || _rotation == 3) && _width != _height) {
int16_t t = _width; _width = _height; _height = t;
}
char buf[12]; snprintf(buf, sizeof(buf), "L:R:%d", (int)r);
Serial.write((uint8_t)2); Serial.print(buf); Serial.write((uint8_t)3);
}
/* ── composite shapes ── */
void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_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 drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c)
{ _rn_lt5('l', x0, y0, x1, y1, c); }
void drawCircle(int16_t x, int16_t y, int16_t r, uint16_t c)
{ _rn_lt5('c', x, y, r, 0, c); }
void fillCircle(int16_t x, int16_t y, int16_t r, uint16_t c)
{ _rn_lt5('c', x, y, r, 1, c); }
void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, 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 r, uint16_t c)
{ fillRect(x,y,w,h,c); }
void fillTriangle(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 drawTriangle(int16_t x0,int16_t y0,int16_t x1,int16_t y1,int16_t x2,int16_t y2,uint16_t c)
{ fillTriangle(x0,y0,x1,y1,x2,y2,c); }
/* ── bitmap ── */
void drawBitmap(int16_t x,int16_t y,const uint8_t* bmp,int16_t w,int16_t h,uint16_t c,uint16_t bg=0) {
int16_t bw = (w+7)/8;
for (int16_t j=0;j<h;j++) for (int16_t i=0;i<w;i++) {
if (bmp[j*bw+i/8] & (0x80>>(i&7))) drawPixel(x+i,y+j,c);
else if (bg!=c) drawPixel(x+i,y+j,bg);
}
}
/* ── text state ── */
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; }
int16_t getCursorX() const { return _cx; }
int16_t getCursorY() const { return _cy; }
void setFont(const GFXfont*) {}
void setTextWrap(bool) {}
int16_t textWidth(const char* s) { return s?(int16_t)(strlen(s)*6*_ts):0; }
/* ── character output → L:t telemetry ── */
size_t write(uint8_t c) override {
if (c == '\\n') { _cy+=8*_ts; _cx=0; return 1; }
if (c == '\\r') return 1;
char buf[40];
snprintf(buf,sizeof(buf),"L:t:%d,%d,%d,%d,%d,%d",(int)_cx,(int)_cy,(int)_fg,(int)_bg,(int)_ts,(int)c);
Serial.write((uint8_t)2); Serial.print(buf); Serial.write((uint8_t)3);
_cx += 6*_ts;
return 1;
}
/* ── color utility ── */
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);
}
};
/* ══════════════════════════════════════════════════════
Adafruit_ILI9341
══════════════════════════════════════════════════════ */
class Adafruit_ILI9341 : public Adafruit_GFX {
public:
/* hardware-pin constructor (cs, dc, rst) */
Adafruit_ILI9341(int8_t cs=-1, int8_t dc=-1, int8_t rst=-1)
: Adafruit_GFX(240, 320) {}
/* software-SPI constructor (cs, dc, mosi, clk, rst, miso) */
Adafruit_ILI9341(int8_t cs, int8_t dc, int8_t mosi, int8_t clk, int8_t rst, int8_t miso=-1)
: Adafruit_GFX(240, 320) {}
/* SPI-object constructor */
Adafruit_ILI9341(void* spi, int8_t dc, int8_t cs=-1, int8_t rst=-1)
: Adafruit_GFX(240, 320) {}
void begin(uint32_t freq=0) {
Serial.write((uint8_t)2);
Serial.print("L:I:240,320");
Serial.write((uint8_t)3);
}
void startWrite() {}
void endWrite() {}
void invertDisplay(bool) {}
void scrollTo(uint16_t) {}
uint16_t readPixel(int16_t, int16_t) { return 0; }
uint8_t readcommand8(uint8_t, uint8_t=0) { return 0; }
};
#endif /* _RN_ILI9341_MOCK_H */
`;
+79 -16
View File
@@ -12,6 +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';
export function registerArduinoRoutes(app: express.Application, getSessionUser: (req: express.Request) => any): void { export function registerArduinoRoutes(app: express.Application, getSessionUser: (req: express.Request) => any): void {
@@ -203,9 +204,8 @@ const _ESP8266_FEATURED: Record<string, string> = {
// STM32 대표 보드 (검색 없을 때만 적용, Renode 에뮬레이터 지원) // STM32 대표 보드 (검색 없을 때만 적용, Renode 에뮬레이터 지원)
const _STM32_FEATURED: Record<string, string> = { const _STM32_FEATURED: Record<string, string> = {
'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8': 'Blue Pill (STM32F103C8)', 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8': '블루필 (STM32F103C8)',
'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE': 'Black Pill (STM32F411CE)', 'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE': '블랙필 (STM32F411CE)',
'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F401CC': 'Black Pill (STM32F401CC)',
'STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F103RB': 'Nucleo-64 F103RB', 'STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F103RB': 'Nucleo-64 F103RB',
'STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F401RE': 'Nucleo-64 F401RE', 'STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F401RE': 'Nucleo-64 F401RE',
'STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F446RE': 'Nucleo-64 F446RE', 'STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F446RE': 'Nucleo-64 F446RE',
@@ -1908,6 +1908,7 @@ interface RenodeSession {
tmpDir: string; tmpDir: string;
fqbn: string; fqbn: string;
sseConns: Set<any>; sseConns: Set<any>;
uartBuf: string[]; // SSE 연결 전 도착한 데이터 재전송 버퍼
timer: ReturnType<typeof setTimeout>; timer: ReturnType<typeof setTimeout>;
idleTimer?: ReturnType<typeof setTimeout>; idleTimer?: ReturnType<typeof setTimeout>;
} }
@@ -1935,7 +1936,8 @@ app.post('/api/arduino/renode/start', async (req, res) => {
const id = 'renode_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); const id = 'renode_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
const tmpDir = require('os').tmpdir() + '/' + id; const tmpDir = require('os').tmpdir() + '/' + id;
require('fs').mkdirSync(tmpDir, { recursive: true }); require('fs').mkdirSync(tmpDir, { recursive: true });
const fqbn = rawFqbn && /^[\w\-:.]+$/.test(rawFqbn) ? rawFqbn : 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8'; // Renode는 항상 Blue Pill(STM32F103C8) 플랫폼 — 사용자 FQBN 무관하게 고정
const fqbn = 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8';
try { try {
const cliPath = await getArduinoCli(); const cliPath = await getArduinoCli();
@@ -1971,6 +1973,46 @@ app.post('/api/arduino/renode/start', async (req, res) => {
'#define _RN_DW(p,v) do{uint32_t _rv=(uint32_t)(v);digitalWrite(p,_rv);Serial.write(2);Serial.print("P:");Serial.print(_RN_PNAME((uint32_t)(p)));Serial.print(":");Serial.print(_rv);Serial.write(3);}while(0)', '#define _RN_DW(p,v) do{uint32_t _rv=(uint32_t)(v);digitalWrite(p,_rv);Serial.write(2);Serial.print("P:");Serial.print(_RN_PNAME((uint32_t)(p)));Serial.print(":");Serial.print(_rv);Serial.write(3);}while(0)',
].join('\n') + '\n'; ].join('\n') + '\n';
patchedCode = gpioHeader + patchedCode.replace(/\bdigitalWrite\s*\(/g, '_RN_DW('); patchedCode = gpioHeader + patchedCode.replace(/\bdigitalWrite\s*\(/g, '_RN_DW(');
// _RN_DW가 Serial.write()를 호출하므로 setup() 첫 줄에 Serial.begin() 보장
// 스케치에 이미 Serial.begin()이 있어도 중복 호출은 무해함
if (!/Serial\.begin\s*\(/.test(patchedCode)) {
patchedCode = patchedCode.replace(
/void\s+setup\s*\(\s*\)\s*\{/,
'void setup() {\n Serial.begin(115200);'
);
}
// ILI9341 / GFX 사용 시: 실제 라이브러리를 로컬 mock으로 교체 (SPI 없음)
const _hasILI9341 = /Adafruit_ILI9341\.h/.test(patchedCode);
if (_hasILI9341) {
const fs2 = require('fs');
// mock 헤더 파일들 스케치 디렉토리에 배치
fs2.writeFileSync(sketchDir + '/Adafruit_ILI9341.h', _RN_ILI9341_MOCK_H);
fs2.writeFileSync(sketchDir + '/Adafruit_GFX.h', '#pragma once\n#include "Adafruit_ILI9341.h"\n');
fs2.writeFileSync(sketchDir + '/Adafruit_SPITFT.h', '#pragma once\n#include "Adafruit_ILI9341.h"\n');
// SPI.h stub — 실제 SPI 하드웨어 접근 차단 (Renode에 SPI 주변장치 없음)
fs2.writeFileSync(sketchDir + '/SPI.h',
'#pragma once\n#include <stdint.h>\n' +
'struct SPISettings { SPISettings(uint32_t,uint8_t,uint8_t){} };\n' +
'class SPIClass {\npublic:\n' +
' void begin(){} void end(){}\n' +
' void beginTransaction(const SPISettings&){}\n' +
' void endTransaction(){}\n' +
' uint8_t transfer(uint8_t){return 0;}\n' +
' uint16_t transfer16(uint16_t){return 0;}\n' +
'};\nextern SPIClass SPI;\n'
);
// <...> 형식 include를 로컬 "..." 형식으로 교체하여 시스템 라이브러리 무시
patchedCode = patchedCode
.replace(/#include\s*<Adafruit_ILI9341\.h>/g, '#include "Adafruit_ILI9341.h"')
.replace(/#include\s*<Adafruit_GFX\.h>/g, '#include "Adafruit_GFX.h"')
.replace(/#include\s*<Adafruit_SPITFT\.h>/g, '#include "Adafruit_SPITFT.h"')
.replace(/#include\s*<SPI\.h>/g, '#include "SPI.h"')
// &SPI1/&SPI2: cast-macro라 address-of 연산 불가 → nullptr (mock은 SPI 포인터 무시)
.replace(/&(SPI\d+)\b/g, 'nullptr');
}
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 }> =>
@@ -2020,17 +2062,40 @@ app.post('/api/arduino/renode/start', async (req, res) => {
const session: RenodeSession = { const session: RenodeSession = {
proc, socket: null, tmpDir, fqbn, proc, socket: null, tmpDir, fqbn,
sseConns: new Set(), sseConns: new Set(),
uartBuf: [],
timer: setTimeout(() => killRenodeSession(id), 30 * 60 * 1000), timer: setTimeout(() => killRenodeSession(id), 30 * 60 * 1000),
}; };
renodeSessions.set(id, session); renodeSessions.set(id, session);
proc.stdout.on('data', () => {}); const _ssePush = (msg: string) => {
proc.stderr.on('data', (d: Buffer) => { if (session.uartBuf.length >= 200) session.uartBuf.shift();
const txt = d.toString('utf-8'); session.uartBuf.push(msg);
console.error('[Renode]', txt.trim());
const msg = 'data: ' + JSON.stringify({ serial: '[Renode] ' + txt }) + '\n\n';
for (const conn of session.sseConns) try { conn.write(msg); } catch {} for (const conn of session.sseConns) try { conn.write(msg); } catch {}
}); };
// Renode stdout/stderr는 작은 chunk로 오고 ANSI 코드 포함 — 라인 단위로 버퍼링 후 전송
// WARNING 라인은 콘솔에만 기록하고 시리얼 모니터에는 표시 안 함 (HAL 초기화 노이즈)
let _rnLineBuf = '';
const _rnLog = (d: Buffer) => {
_rnLineBuf += d.toString('utf-8').replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''); // strip ANSI
let nl: number;
while ((nl = _rnLineBuf.indexOf('\n')) >= 0) {
const line = _rnLineBuf.slice(0, nl + 1).replace(/\r/g, '');
_rnLineBuf = _rnLineBuf.slice(nl + 1);
const trimmed = line.trim();
if (!trimmed) continue;
console.error('[Renode]', trimmed);
// 시리얼 모니터에는 "Machine started"와 ERROR만 표시 — 나머지는 콘솔 전용
if (trimmed.includes('Machine started')) {
// 타임스탬프·[INFO] 제거, 앞뒤 빈 줄로 스케치 출력과 구분
const clean = trimmed.replace(/^\S+\s+\[INFO\]\s*/, '');
_ssePush('data: ' + JSON.stringify({ serial: '\n[Renode] ' + clean + '\n\n' }) + '\n\n');
} else if (trimmed.includes('[ERROR]')) {
_ssePush('data: ' + JSON.stringify({ serial: '[Renode] ' + trimmed + '\n' }) + '\n\n');
}
}
};
proc.stdout.on('data', _rnLog);
proc.stderr.on('data', _rnLog);
proc.on('exit', (code: number | null) => { proc.on('exit', (code: number | null) => {
console.error('[Renode] exited', code); console.error('[Renode] exited', code);
const msg = 'data: ' + JSON.stringify({ exit: code ?? 0 }) + '\n\n'; const msg = 'data: ' + JSON.stringify({ exit: code ?? 0 }) + '\n\n';
@@ -2052,13 +2117,10 @@ app.post('/api/arduino/renode/start', async (req, res) => {
connectUart().then(sock => { connectUart().then(sock => {
console.log(`[Renode UART] connected: ${id}, sseConns=${session.sseConns.size}`); console.log(`[Renode UART] connected: ${id}, sseConns=${session.sseConns.size}`);
session.socket = sock; session.socket = sock;
const connMsg = 'data: ' + JSON.stringify({ serial: '[Renode] UART 연결됨\n' }) + '\n\n'; _ssePush('data: ' + JSON.stringify({ serial: '[Renode] UART 연결됨\n' }) + '\n\n');
for (const conn of session.sseConns) try { conn.write(connMsg); } catch {}
sock.on('data', (chunk: Buffer) => { sock.on('data', (chunk: Buffer) => {
const text = chunk.toString('utf-8'); const text = chunk.toString('utf-8');
console.log(`[Renode UART] ${text.length}B → ${session.sseConns.size} conns`); _ssePush('data: ' + JSON.stringify({ serial: text }) + '\n\n');
const msg = 'data: ' + JSON.stringify({ serial: text }) + '\n\n';
for (const conn of session.sseConns) try { conn.write(msg); } catch {}
}); });
sock.on('close', () => { sock.on('close', () => {
console.log(`[Renode UART] closed: ${id}`); console.log(`[Renode UART] closed: ${id}`);
@@ -2092,7 +2154,8 @@ app.get('/api/arduino/renode/:id/output', (req, res) => {
'X-Accel-Buffering': 'no', 'X-Accel-Buffering': 'no',
}); });
res.write('\n'); res.write('\n');
if (s.socket) res.write('data: ' + JSON.stringify({ serial: '[Renode] UART 연결됨\n' }) + '\n\n'); // 버퍼에 쌓인 데이터 재전송 (SSE 연결 전 도착한 UART/로그 데이터)
for (const msg of s.uartBuf) try { res.write(msg); } catch {}
s.sseConns.add(res); s.sseConns.add(res);
if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = undefined; } if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = undefined; }
req.on('close', () => { req.on('close', () => {
+37 -3
View File
@@ -3083,8 +3083,8 @@ function _stm32PinResolve(s) {
} }
let _renodeGpioBuf = ''; let _renodeGpioBuf = '';
let _stm32LcdInited = false;
function _stm32ParseGpio(raw) { function _stm32ParseGpio(raw) {
// 텔레메트리: STX(0x02) "P:name:val" ETX(0x03) 패킷 추출, 나머지는 시리얼로
_renodeGpioBuf += raw; _renodeGpioBuf += raw;
let out = ''; let out = '';
let i = 0; let i = 0;
@@ -3093,17 +3093,45 @@ function _stm32ParseGpio(raw) {
if (s < 0) { out += _renodeGpioBuf.slice(i); _renodeGpioBuf = ''; break; } if (s < 0) { out += _renodeGpioBuf.slice(i); _renodeGpioBuf = ''; break; }
out += _renodeGpioBuf.slice(i, s); out += _renodeGpioBuf.slice(i, s);
const e = _renodeGpioBuf.indexOf('\x03', s + 1); const e = _renodeGpioBuf.indexOf('\x03', s + 1);
if (e < 0) { _renodeGpioBuf = _renodeGpioBuf.slice(s); break; } // 패킷 미완성 if (e < 0) { _renodeGpioBuf = _renodeGpioBuf.slice(s); break; }
const pkt = _renodeGpioBuf.slice(s + 1, e); // "P:PA0:1" const pkt = _renodeGpioBuf.slice(s + 1, e);
_renodeGpioBuf = _renodeGpioBuf.slice(e + 1); _renodeGpioBuf = _renodeGpioBuf.slice(e + 1);
i = 0; i = 0;
const parts = pkt.split(':'); const parts = pkt.split(':');
if (parts[0] === 'P' && parts.length >= 3) if (parts[0] === 'P' && parts.length >= 3)
_stm32UpdatePin(_stm32PinResolve(parts[1]), parts[2]); _stm32UpdatePin(_stm32PinResolve(parts[1]), parts[2]);
else if (parts[0] === 'L' && parts.length >= 3)
_stm32LcdDraw(parts[1], parts.slice(2).join(':'));
} }
return out; return out;
} }
function _stm32LcdDraw(op, data) {
if (op === 'I') {
const [w, h] = data.split(',').map(Number);
_esp32TftW = w || 240; _esp32TftH = h || 320; _esp32TftCtx2d = null;
_esp32TftDraw('I:' + _esp32TftW + ',' + _esp32TftH);
_stm32LcdInited = true;
return;
}
if (op === 'R') { _esp32TftDraw('R:' + parseInt(data)); return; }
if (!_stm32LcdInited) {
_stm32LcdInited = true;
_esp32TftW = 240; _esp32TftH = 320; _esp32TftCtx2d = null;
_esp32TftDraw('I:240,320');
}
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
}
}
function _buildBluePillSVG(isF4) { function _buildBluePillSVG(isF4) {
const leftPins = [ const leftPins = [
['GND','#666'],['GND','#666'],['3V3','#c0c090'],['NRST','#b0b090'], ['GND','#666'],['GND','#666'],['3V3','#c0c090'],['NRST','#b0b090'],
@@ -3170,6 +3198,12 @@ function _buildBluePillSVG(isF4) {
function initSTM32RenodeMode(sessionId, fname, fqbn) { function initSTM32RenodeMode(sessionId, fname, fqbn) {
_renodeSessionId = sessionId; _renodeSessionId = sessionId;
_renodeGpioBuf = ''; _renodeGpioBuf = '';
_stm32LcdInited = false;
_esp32TftCtx2d = null;
const _tftSec = document.getElementById('tft-section');
if (_tftSec) _tftSec.style.display = 'none';
const _tftDot = document.getElementById('tft-dot');
if (_tftDot) _tftDot.classList.remove('on');
document.querySelector('.header h1').textContent = '🖥️ STM32 Emulator (Renode)'; document.querySelector('.header h1').textContent = '🖥️ STM32 Emulator (Renode)';
document.getElementById('filename').textContent = fname; document.getElementById('filename').textContent = fname;