mirror of
https://github.com/Vale54321/schafkop-neu.git
synced 2025-12-13 02:29: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`);
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const offData = this.serial.on('data', (line) => sendEvent('data', { line }));
|
const offData = this.serial.on('data', (line) => sendEvent('data', line));
|
||||||
const offOpen = this.serial.on('open', () => sendEvent('open', { ts: Date.now() }));
|
const offOpen = this.serial.on('open', () => sendEvent('open', ''));
|
||||||
const offClose = this.serial.on('close', () => sendEvent('close', { ts: Date.now() }));
|
const offClose = this.serial.on('close', () => sendEvent('close', ''));
|
||||||
const offErr = this.serial.on('error', (err) => sendEvent('error', { message: String(err) }));
|
const offErr = this.serial.on('error', (err) => sendEvent('error', String(err)));
|
||||||
|
|
||||||
console.log('[SerialController] SSE client connected');
|
console.log('[SerialController] SSE client connected');
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ export class SerialService implements OnModuleDestroy, OnModuleInit {
|
|||||||
private emitter = new EventEmitter();
|
private emitter = new EventEmitter();
|
||||||
private port: any = null;
|
private port: any = null;
|
||||||
private parser: 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[]> {
|
async listPorts(): Promise<PortInfo[]> {
|
||||||
console.log('[SerialService] listPorts()');
|
console.log('[SerialService] listPorts()');
|
||||||
@@ -70,9 +87,11 @@ export class SerialService implements OnModuleDestroy, OnModuleInit {
|
|||||||
console.error('[SerialService] port error', err);
|
console.error('[SerialService] port error', err);
|
||||||
this.emitter.emit('error', err?.message ?? String(err));
|
this.emitter.emit('error', err?.message ?? String(err));
|
||||||
});
|
});
|
||||||
|
this.port.on('close', () => { this.emitter.emit('close'); });
|
||||||
this.parser.on('data', (line: string) => {
|
this.parser.on('data', (line: string) => {
|
||||||
console.log('[SerialService] RX:', line);
|
const clean = line.replace(/[\r\n]+$/, '');
|
||||||
this.emitter.emit('data', line);
|
if (this.verbose) console.log('[SerialService] RX:', clean);
|
||||||
|
this.emitter.emit('data', clean);
|
||||||
});
|
});
|
||||||
|
|
||||||
// open the port
|
// open the port
|
||||||
@@ -134,6 +153,11 @@ export class SerialService implements OnModuleDestroy, OnModuleInit {
|
|||||||
this.emitter.on(event, cb);
|
this.emitter.on(event, cb);
|
||||||
return () => this.emitter.off(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() {
|
onModuleDestroy() {
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -74,13 +74,20 @@
|
|||||||
// data events contain the actual lines emitted by the Pico; parse JSON payloads but fall back to raw
|
// data events contain the actual lines emitted by the Pico; parse JSON payloads but fall back to raw
|
||||||
es.addEventListener('data', (ev: any) => {
|
es.addEventListener('data', (ev: any) => {
|
||||||
console.log('[serial][SSE] data event', ev.data);
|
console.log('[serial][SSE] data event', ev.data);
|
||||||
|
const raw = ev.data as string;
|
||||||
|
let line: string | undefined;
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(ev.data);
|
const parsed = JSON.parse(raw);
|
||||||
// always show the raw line emitted by the firmware
|
if (parsed && typeof parsed === 'object' && 'line' in parsed) {
|
||||||
addLog('RX: ' + d.line);
|
line = (parsed as any).line;
|
||||||
} catch (e) {
|
} else if (typeof parsed === 'string') {
|
||||||
addLog('RX (raw): ' + ev.data);
|
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]
|
[env:pico]
|
||||||
platform = raspberrypi
|
platform = raspberrypi
|
||||||
board = pico
|
board = pico
|
||||||
framework = arduino
|
framework = arduino
|
||||||
; build_flags removed, no longer needed
|
|
||||||
@@ -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 <Arduino.h>
|
||||||
#include "Stepper.h"
|
#include "Stepper.h"
|
||||||
#include "ULN2003Stepper.h"
|
#include "ULN2003Stepper.h"
|
||||||
|
|
||||||
|
#ifndef LED_BUILTIN
|
||||||
|
#define LED_BUILTIN 25
|
||||||
|
#endif
|
||||||
|
|
||||||
ULN2003Stepper driver1({18, 19, 20, 21}, 4096);
|
ULN2003Stepper driver1({18, 19, 20, 21}, 4096);
|
||||||
int revSteps = 0;
|
|
||||||
|
|
||||||
// Generic IO pointers (assigned to USB Serial or Serial1 at runtime)
|
class SchafkopfSerialApp {
|
||||||
Stream* serialIn = nullptr; // for available()/read()
|
public:
|
||||||
Print* serialOut = nullptr; // for print()/println()
|
void begin(uint32_t baud = 115200) {
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
|
|
||||||
void setup() {
|
Serial.begin(baud);
|
||||||
// Start USB CDC first
|
|
||||||
Serial.begin(115200);
|
int revSteps = driver1.get_steps_per_rev();
|
||||||
unsigned long start = millis();
|
Serial.print("EVENT:START STEPS_PER_REV ");
|
||||||
while (!Serial && (millis() - start) < 2000UL) {
|
Serial.println(revSteps);
|
||||||
// wait up to 2 seconds for host to open USB (non-blocking fallback)
|
|
||||||
|
waitForHost(1500);
|
||||||
|
logStartup();
|
||||||
}
|
}
|
||||||
if (Serial) {
|
|
||||||
serialIn = &Serial;
|
void loop() {
|
||||||
serialOut = &Serial;
|
pollSerial();
|
||||||
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)");
|
|
||||||
}
|
}
|
||||||
revSteps = driver1.get_steps_per_rev();
|
|
||||||
serialOut->print("EVENT:START STEPS_PER_REV ");
|
|
||||||
serialOut->println(revSteps);
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper: trim leading/trailing whitespace
|
private:
|
||||||
static String trim(const String &s) {
|
String inputBuffer;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
enum class CmdType { PING, STEP, SPEED, UNKNOWN };
|
||||||
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);
|
|
||||||
|
|
||||||
bool success = true;
|
void waitForHost(unsigned long timeoutMs){
|
||||||
String result = "OK";
|
unsigned long start = millis();
|
||||||
|
while(!Serial && (millis()-start) < timeoutMs) { /* wait */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Parse command
|
void blink(uint16_t onMs = 40, uint16_t offMs = 0){
|
||||||
if (cmd.startsWith("STEP ")) {
|
digitalWrite(LED_BUILTIN, HIGH);
|
||||||
int idx1 = cmd.indexOf(' ');
|
delay(onMs);
|
||||||
int idx2 = cmd.indexOf(' ', idx1 + 1);
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
if (idx1 != -1 && idx2 != -1) {
|
if(offMs) delay(offMs);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// always print a short status summary
|
void logStartup(){
|
||||||
if (success) {
|
Serial.println(F("HELLO START"));
|
||||||
serialOut->print("STATUS: ");
|
for(int i=0;i<3;i++) blink(60,100);
|
||||||
serialOut->println("OK");
|
}
|
||||||
} else {
|
|
||||||
serialOut->print("STATUS: ");
|
void pollSerial(){
|
||||||
serialOut->println(result);
|
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%
|
call :install frontend %FRONTEND_PM%
|
||||||
|
|
||||||
REM Set backend environment variables (edit as needed)
|
REM Set backend environment variables (edit as needed)
|
||||||
set "SERIAL_PORT=COM4"
|
set "SERIAL_PORT=COM6"
|
||||||
set "SERIAL_BAUD=115200"
|
set "SERIAL_BAUD=115200"
|
||||||
echo Using SERIAL_PORT=%SERIAL_PORT% SERIAL_BAUD=%SERIAL_BAUD%
|
echo Using SERIAL_PORT=%SERIAL_PORT% SERIAL_BAUD=%SERIAL_BAUD%
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user