feat: enhance serial communication handling and logging in backend services

This commit is contained in:
2025-09-02 23:25:25 +02:00
parent 423eaefe85
commit db44b15717
6 changed files with 179 additions and 121 deletions

View File

@@ -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');

View File

@@ -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<PortInfo[]> {
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();

View File

@@ -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);
});
}

View File

@@ -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

View File

@@ -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 <Arduino.h>
#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 <steps> <dir>
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 <steps> <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 <positive_delay_us>"));
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(); }

View File

@@ -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%