From 5e9596e188f7f5ba87f65c87114f96a0bda52994 Mon Sep 17 00:00:00 2001 From: kim Date: Tue, 30 Jun 2026 13:52:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20STM32=20Renode=20=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=96=BC=20=EC=B6=9C=EB=A0=A5=20=EC=88=98=EC=A0=95=20+=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/gateway/mocks-stm32.ts | 186 ++++++++++++++++++++++++++++++++++ src/gateway/routes-arduino.ts | 95 ++++++++++++++--- web-ui/arduino-emulator.html | 40 +++++++- 3 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 src/gateway/mocks-stm32.ts diff --git a/src/gateway/mocks-stm32.ts b/src/gateway/mocks-stm32.ts new file mode 100644 index 0000000..bbf52dc --- /dev/null +++ b/src/gateway/mocks-stm32.ts @@ -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 + +/* ── 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>(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 */ +`; diff --git a/src/gateway/routes-arduino.ts b/src/gateway/routes-arduino.ts index c52cf30..b41982e 100644 --- a/src/gateway/routes-arduino.ts +++ b/src/gateway/routes-arduino.ts @@ -12,6 +12,7 @@ import { _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, } from './mocks-esp32'; +import { _RN_ILI9341_MOCK_H } from './mocks-stm32'; export function registerArduinoRoutes(app: express.Application, getSessionUser: (req: express.Request) => any): void { @@ -203,9 +204,8 @@ const _ESP8266_FEATURED: Record = { // STM32 대표 보드 (검색 없을 때만 적용, Renode 에뮬레이터 지원) const _STM32_FEATURED: Record = { - 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8': 'Blue Pill (STM32F103C8)', - 'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE': 'Black Pill (STM32F411CE)', - 'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F401CC': 'Black Pill (STM32F401CC)', + 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8': '블루필 (STM32F103C8)', + 'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE': '블랙필 (STM32F411CE)', '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_F446RE': 'Nucleo-64 F446RE', @@ -1908,6 +1908,7 @@ interface RenodeSession { tmpDir: string; fqbn: string; sseConns: Set; + uartBuf: string[]; // SSE 연결 전 도착한 데이터 재전송 버퍼 timer: ReturnType; idleTimer?: ReturnType; } @@ -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 tmpDir = require('os').tmpdir() + '/' + id; 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 { 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)', ].join('\n') + '\n'; 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 \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*/g, '#include "Adafruit_ILI9341.h"') + .replace(/#include\s*/g, '#include "Adafruit_GFX.h"') + .replace(/#include\s*/g, '#include "Adafruit_SPITFT.h"') + .replace(/#include\s*/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); 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 = { proc, socket: null, tmpDir, fqbn, sseConns: new Set(), + uartBuf: [], timer: setTimeout(() => killRenodeSession(id), 30 * 60 * 1000), }; renodeSessions.set(id, session); - proc.stdout.on('data', () => {}); - proc.stderr.on('data', (d: Buffer) => { - const txt = d.toString('utf-8'); - console.error('[Renode]', txt.trim()); - const msg = 'data: ' + JSON.stringify({ serial: '[Renode] ' + txt }) + '\n\n'; + const _ssePush = (msg: string) => { + if (session.uartBuf.length >= 200) session.uartBuf.shift(); + session.uartBuf.push(msg); 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) => { console.error('[Renode] exited', code); 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 => { console.log(`[Renode UART] connected: ${id}, sseConns=${session.sseConns.size}`); session.socket = sock; - const connMsg = 'data: ' + JSON.stringify({ serial: '[Renode] UART 연결됨\n' }) + '\n\n'; - for (const conn of session.sseConns) try { conn.write(connMsg); } catch {} + _ssePush('data: ' + JSON.stringify({ serial: '[Renode] UART 연결됨\n' }) + '\n\n'); sock.on('data', (chunk: Buffer) => { const text = chunk.toString('utf-8'); - console.log(`[Renode UART] ${text.length}B → ${session.sseConns.size} conns`); - const msg = 'data: ' + JSON.stringify({ serial: text }) + '\n\n'; - for (const conn of session.sseConns) try { conn.write(msg); } catch {} + _ssePush('data: ' + JSON.stringify({ serial: text }) + '\n\n'); }); sock.on('close', () => { console.log(`[Renode UART] closed: ${id}`); @@ -2092,7 +2154,8 @@ app.get('/api/arduino/renode/:id/output', (req, res) => { 'X-Accel-Buffering': 'no', }); 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); if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = undefined; } req.on('close', () => { diff --git a/web-ui/arduino-emulator.html b/web-ui/arduino-emulator.html index 391ffe7..6f4615c 100644 --- a/web-ui/arduino-emulator.html +++ b/web-ui/arduino-emulator.html @@ -3083,8 +3083,8 @@ function _stm32PinResolve(s) { } let _renodeGpioBuf = ''; +let _stm32LcdInited = false; function _stm32ParseGpio(raw) { - // 텔레메트리: STX(0x02) "P:name:val" ETX(0x03) 패킷 추출, 나머지는 시리얼로 _renodeGpioBuf += raw; let out = ''; let i = 0; @@ -3093,17 +3093,45 @@ function _stm32ParseGpio(raw) { if (s < 0) { out += _renodeGpioBuf.slice(i); _renodeGpioBuf = ''; break; } out += _renodeGpioBuf.slice(i, s); const e = _renodeGpioBuf.indexOf('\x03', s + 1); - if (e < 0) { _renodeGpioBuf = _renodeGpioBuf.slice(s); break; } // 패킷 미완성 - const pkt = _renodeGpioBuf.slice(s + 1, e); // "P:PA0:1" + if (e < 0) { _renodeGpioBuf = _renodeGpioBuf.slice(s); break; } + const pkt = _renodeGpioBuf.slice(s + 1, e); _renodeGpioBuf = _renodeGpioBuf.slice(e + 1); i = 0; const parts = pkt.split(':'); if (parts[0] === 'P' && parts.length >= 3) _stm32UpdatePin(_stm32PinResolve(parts[1]), parts[2]); + else if (parts[0] === 'L' && parts.length >= 3) + _stm32LcdDraw(parts[1], parts.slice(2).join(':')); } 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) { const leftPins = [ ['GND','#666'],['GND','#666'],['3V3','#c0c090'],['NRST','#b0b090'], @@ -3170,6 +3198,12 @@ function _buildBluePillSVG(isF4) { function initSTM32RenodeMode(sessionId, fname, fqbn) { _renodeSessionId = sessionId; _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.getElementById('filename').textContent = fname;