mirror of
https://github.com/Vale54321/schafkop-neu.git
synced 2025-12-11 09:59:33 +01:00
feat: enhance serial communication handling and logging in backend services
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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%
|
||||
|
||||
|
||||
Reference in New Issue
Block a user