From db44b15717dd4883630d841ce4006e4278c70b1d Mon Sep 17 00:00:00 2001 From: Valentin Heiserer Date: Tue, 2 Sep 2025 23:25:25 +0200 Subject: [PATCH] feat: enhance serial communication handling and logging in backend services --- backend/src/serial/serial.controller.ts | 8 +- backend/src/serial/serial.service.ts | 28 ++- frontend/src/App.svelte | 17 +- pico/platformio.ini | 13 +- pico/src/main.cpp | 232 ++++++++++++++---------- start-dev.bat | 2 +- 6 files changed, 179 insertions(+), 121 deletions(-) diff --git a/backend/src/serial/serial.controller.ts b/backend/src/serial/serial.controller.ts index a78f3b1..1b1be4d 100644 --- a/backend/src/serial/serial.controller.ts +++ b/backend/src/serial/serial.controller.ts @@ -65,10 +65,10 @@ export class SerialController { res.write(`data: ${JSON.stringify(data)}\n\n`); }; - const offData = this.serial.on('data', (line) => sendEvent('data', { line })); - const offOpen = this.serial.on('open', () => sendEvent('open', { ts: Date.now() })); - const offClose = this.serial.on('close', () => sendEvent('close', { ts: Date.now() })); - const offErr = this.serial.on('error', (err) => sendEvent('error', { message: String(err) })); + const offData = this.serial.on('data', (line) => sendEvent('data', line)); + const offOpen = this.serial.on('open', () => sendEvent('open', '')); + const offClose = this.serial.on('close', () => sendEvent('close', '')); + const offErr = this.serial.on('error', (err) => sendEvent('error', String(err))); console.log('[SerialController] SSE client connected'); diff --git a/backend/src/serial/serial.service.ts b/backend/src/serial/serial.service.ts index 8ab8884..e336ebd 100644 --- a/backend/src/serial/serial.service.ts +++ b/backend/src/serial/serial.service.ts @@ -22,6 +22,23 @@ export class SerialService implements OnModuleDestroy, OnModuleInit { private emitter = new EventEmitter(); private port: any = null; private parser: any = null; + private verbose = true; // internal debug; does not change emitted payload format + + constructor() { + // Wrap emitter.emit to log every emitted event centrally + const origEmit = this.emitter.emit.bind(this.emitter); + this.emitter.emit = (event: string, ...args: any[]) => { + if (this.verbose) { + try { + const printable = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)); + console.log('[SerialService][EVENT]', event, printable.join(' ')); + } catch { + console.log('[SerialService][EVENT]', event, args.length, 'arg(s)'); + } + } + return origEmit(event, ...args); + }; + } async listPorts(): Promise { console.log('[SerialService] listPorts()'); @@ -70,9 +87,11 @@ export class SerialService implements OnModuleDestroy, OnModuleInit { console.error('[SerialService] port error', err); this.emitter.emit('error', err?.message ?? String(err)); }); + this.port.on('close', () => { this.emitter.emit('close'); }); this.parser.on('data', (line: string) => { - console.log('[SerialService] RX:', line); - this.emitter.emit('data', line); + const clean = line.replace(/[\r\n]+$/, ''); + if (this.verbose) console.log('[SerialService] RX:', clean); + this.emitter.emit('data', clean); }); // open the port @@ -134,6 +153,11 @@ export class SerialService implements OnModuleDestroy, OnModuleInit { this.emitter.on(event, cb); return () => this.emitter.off(event, cb); } + // Allow subscription to extended events (disconnect, drain, etc.) + onAny(event: string, cb: (...args: any[]) => void) { + this.emitter.on(event, cb); + return () => this.emitter.off(event, cb); + } onModuleDestroy() { this.close(); diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index fd889b5..03b9c5d 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -74,13 +74,20 @@ // data events contain the actual lines emitted by the Pico; parse JSON payloads but fall back to raw es.addEventListener('data', (ev: any) => { console.log('[serial][SSE] data event', ev.data); + const raw = ev.data as string; + let line: string | undefined; try { - const d = JSON.parse(ev.data); - // always show the raw line emitted by the firmware - addLog('RX: ' + d.line); - } catch (e) { - addLog('RX (raw): ' + ev.data); + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && 'line' in parsed) { + line = (parsed as any).line; + } else if (typeof parsed === 'string') { + line = parsed; // backend sent a plain JSON string + } + } catch { + // not JSON -> treat as plain text } + if (!line) line = raw; + addLog('RX: ' + line); }); } diff --git a/pico/platformio.ini b/pico/platformio.ini index cab2553..feadd21 100644 --- a/pico/platformio.ini +++ b/pico/platformio.ini @@ -1,15 +1,4 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - [env:pico] platform = raspberrypi board = pico -framework = arduino -; build_flags removed, no longer needed +framework = arduino \ No newline at end of file diff --git a/pico/src/main.cpp b/pico/src/main.cpp index 5b24ec0..a7a995b 100644 --- a/pico/src/main.cpp +++ b/pico/src/main.cpp @@ -1,114 +1,152 @@ -// Supports two transport modes: -// 1. USB CDC (Serial) when connected over USB (preferred) -// 2. Hardware UART0 (Serial1) on GPIO pins for headless Raspberry Pi connection -// Wiring for UART mode (3.3V logic only): -// Pi GPIO14 (TXD0, physical pin 8) --> Pico GP1 (UART0 RX) -// Pi GPIO15 (RXD0, physical pin 10) <-- Pico GP0 (UART0 TX) -// Pi GND (any) <--------------------> Pico GND -// Optionally power Pico via USB. If powering from Pi 3V3, DO NOT also power via USB simultaneously. - #include #include "Stepper.h" #include "ULN2003Stepper.h" +#ifndef LED_BUILTIN +#define LED_BUILTIN 25 +#endif + ULN2003Stepper driver1({18, 19, 20, 21}, 4096); -int revSteps = 0; -// Generic IO pointers (assigned to USB Serial or Serial1 at runtime) -Stream* serialIn = nullptr; // for available()/read() -Print* serialOut = nullptr; // for print()/println() +class SchafkopfSerialApp { +public: + void begin(uint32_t baud = 115200) { + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); -void setup() { - // Start USB CDC first - Serial.begin(115200); - unsigned long start = millis(); - while (!Serial && (millis() - start) < 2000UL) { - // wait up to 2 seconds for host to open USB (non-blocking fallback) + Serial.begin(baud); + + int revSteps = driver1.get_steps_per_rev(); + Serial.print("EVENT:START STEPS_PER_REV "); + Serial.println(revSteps); + + waitForHost(1500); + logStartup(); } - if (Serial) { - serialIn = &Serial; - serialOut = &Serial; - Serial.println("INFO: Using USB CDC Serial"); - } else { - // Fall back to UART0 on GPIO0 (TX) / GPIO1 (RX) - Serial1.begin(115200); // default pins for UART0 in Arduino-Pico core - serialIn = &Serial1; - serialOut = &Serial1; - Serial1.println("INFO: Using UART0 (Serial1) on GP0(TX)/GP1(RX)"); + + void loop() { + pollSerial(); } - revSteps = driver1.get_steps_per_rev(); - serialOut->print("EVENT:START STEPS_PER_REV "); - serialOut->println(revSteps); -} -// helper: trim leading/trailing whitespace -static String trim(const String &s) { - int start = 0; - int end = s.length() - 1; - while (start <= end && isspace(s.charAt(start))) start++; - while (end >= start && isspace(s.charAt(end))) end--; - if (end < start) return String(""); - return s.substring(start, end + 1); -} +private: + String inputBuffer; -void loop() { - if (!serialIn || !serialOut) return; // safety - static String input = ""; - while (serialIn->available()) { - char c = serialIn->read(); - if (c == '\n' || c == '\r') { - String cmd = trim(input); - if (cmd.length() > 0) { - // Emit event: received - serialOut->print("EVENT:RECEIVED "); - serialOut->println(cmd); + enum class CmdType { PING, STEP, SPEED, UNKNOWN }; - bool success = true; - String result = "OK"; + void waitForHost(unsigned long timeoutMs){ + unsigned long start = millis(); + while(!Serial && (millis()-start) < timeoutMs) { /* wait */ } + } - // Parse command - if (cmd.startsWith("STEP ")) { - int idx1 = cmd.indexOf(' '); - int idx2 = cmd.indexOf(' ', idx1 + 1); - if (idx1 != -1 && idx2 != -1) { - int steps = cmd.substring(idx1 + 1, idx2).toInt(); - int dir = cmd.substring(idx2 + 1).toInt(); - // execute - driver1.step(steps, dir != 0); - // report completion - serialOut->print("EVENT:COMPLETED STEP "); - serialOut->print(steps); - serialOut->print(" "); - serialOut->println(dir); - } else { - success = false; - result = "ERROR: malformed STEP command"; - } - } else if (cmd.startsWith("SPEED ")) { - int delay_us = cmd.substring(6).toInt(); - driver1.setSpeed(delay_us); - serialOut->print("EVENT:COMPLETED SPEED "); - serialOut->println(delay_us); - } else { - success = false; - result = "ERROR: unknown command"; - serialOut->print("EVENT:COMPLETED "); - serialOut->println(result); - } + void blink(uint16_t onMs = 40, uint16_t offMs = 0){ + digitalWrite(LED_BUILTIN, HIGH); + delay(onMs); + digitalWrite(LED_BUILTIN, LOW); + if(offMs) delay(offMs); + } - // always print a short status summary - if (success) { - serialOut->print("STATUS: "); - serialOut->println("OK"); - } else { - serialOut->print("STATUS: "); - serialOut->println(result); - } + void logStartup(){ + Serial.println(F("HELLO START")); + for(int i=0;i<3;i++) blink(60,100); + } + + void pollSerial(){ + while(Serial.available()){ + char c = Serial.read(); + if(c=='\r') continue; // ignore CR + if(c=='\n') { + processLine(inputBuffer); + inputBuffer = ""; + } else if (inputBuffer.length() < 200) { + inputBuffer += c; } - input = ""; - } else { - input += c; } } - delay(10); // avoid busy loop -} + + void processLine(const String &raw){ + String line = raw; + line.trim(); + if(!line.length()) return; + String cmdToken = firstToken(line); + String rest = remainingAfterFirst(line); + CmdType type = classify(cmdToken); + handleCommand(type, cmdToken, rest); + } + + static String firstToken(const String &line){ + int sp = line.indexOf(' '); + return (sp==-1)? line : line.substring(0, sp); + } + static String remainingAfterFirst(const String &line){ + int sp = line.indexOf(' '); + if (sp==-1) return String(""); + String r = line.substring(sp+1); + r.trim(); + return r; + } + + CmdType classify(String token){ + token.toUpperCase(); + if(token==F("PING")) return CmdType::PING; + if(token==F("STEP")) return CmdType::STEP; + if(token==F("SPEED")) return CmdType::SPEED; + return CmdType::UNKNOWN; + } + + void handleCommand(CmdType type, const String &token, const String &args){ + switch(type){ + case CmdType::PING: { + Serial.println(F("PONG")); + blink(); + break; } + case CmdType::STEP: { + // Expected: STEP + int steps = -1; int dir = -1; + if(parseStepArgs(args, steps, dir)) { + Serial.print(F("STEP: moving ")); Serial.print(steps); Serial.print(F(" dir=")); Serial.println(dir); + driver1.step(steps, dir!=0); + blink(60); + } else { + Serial.println(F("ERR:STEP usage STEP <0|1>")); + blink(20); + } + break; } + case CmdType::SPEED: { + int delayUs = args.toInt(); + if(delayUs > 0) { + driver1.setSpeed(delayUs); + Serial.print(F("SPEED: set delay_us=")); Serial.println(delayUs); + blink(); + } else { + Serial.println(F("ERR:SPEED usage SPEED ")); + blink(20); + } + break; } + case CmdType::UNKNOWN: { + Serial.print(F("ERR:UNKNOWN COMMAND '")); Serial.print(token); Serial.println("'"); + blink(20); + break; } + default: { + Serial.println(F("ERR:UNHANDLED")); + blink(20); + break; } + } + } + // Helpers for parsing arguments + bool parseStepArgs(const String &args, int &steps, int &dir){ + int sp = args.indexOf(' '); + if(sp == -1) return false; + String a = args.substring(0, sp); a.trim(); + String b = args.substring(sp+1); b.trim(); + if(!a.length() || !b.length()) return false; + steps = a.toInt(); dir = b.toInt(); + if(steps <= 0) return false; + if(!(dir==0 || dir==1)) return false; + return true; + } +}; + +SchafkopfSerialApp app; + +void setup(){ app.begin(); } +void loop(){ app.loop(); } diff --git a/start-dev.bat b/start-dev.bat index d674961..b7b4bfc 100644 --- a/start-dev.bat +++ b/start-dev.bat @@ -15,7 +15,7 @@ call :install backend %BACKEND_PM% call :install frontend %FRONTEND_PM% REM Set backend environment variables (edit as needed) -set "SERIAL_PORT=COM4" +set "SERIAL_PORT=COM6" set "SERIAL_BAUD=115200" echo Using SERIAL_PORT=%SERIAL_PORT% SERIAL_BAUD=%SERIAL_BAUD%