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:
@@ -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 */
|
||||
`;
|
||||
@@ -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<string, string> = {
|
||||
|
||||
// STM32 대표 보드 (검색 없을 때만 적용, Renode 에뮬레이터 지원)
|
||||
const _STM32_FEATURED: Record<string, string> = {
|
||||
'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<any>;
|
||||
uartBuf: string[]; // SSE 연결 전 도착한 데이터 재전송 버퍼
|
||||
timer: 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 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 <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);
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user