diff --git a/pico/README_PICO_SERIAL.md b/pico/README_PICO_SERIAL.md deleted file mode 100644 index 625bebf..0000000 --- a/pico/README_PICO_SERIAL.md +++ /dev/null @@ -1,280 +0,0 @@ -# Pico Serial Motor Control — README for Copilot/Developers - -Purpose -------- -This file documents the exact serial message contract between the Raspberry Pi Pico firmware and a TypeScript backend. It's written so another Copilot or developer can implement a backend and extend firmware features without guesswork. - -Transport modes ---------------- -The firmware supports two serial transports (selected automatically): - -1. USB CDC (preferred) - - When the Pico is connected over USB and a host opens the port, firmware uses `Serial`. - - Typical device name: - - Windows: `COMx` - - Linux (Pi): `/dev/ttyACM0` - - Vendor/Product IDs: 0x2E8A / 0x000A (can be used for auto-detection). - -2. UART0 on GPIO0/GP0 (TX) and GPIO1/GP1 (RX) - - Activated if USB CDC is not opened within ~2 seconds at boot; firmware falls back to `Serial1`. - - Used for headless integration when the Pico is cabled to a Raspberry Pi's UART pins. - -Wiring (Pico UART0 <-> Raspberry Pi UART) ----------------------------------------- -All signals are 3.3V. DO NOT connect 5V to Pico GPIOs. - -| Function | Pico Pin | Pi (BCM) | Pi Physical Pin | -|----------|----------|----------|-----------------| -| UART0 TX | GP0 | RXD0 (15)| 10 | -| UART0 RX | GP1 | TXD0 (14)| 8 | -| GND | GND | GND | (any GND) | - -Power options: -- Preferred: Power Pico over USB (isolated data + power). Only connect GND and TX/RX for UART logic level link. -- Alternate: Power from Pi 3V3 pin to Pico 3V3 pin (NOT VBUS) plus GND. Never power from USB and Pi 3V3 simultaneously unless you know backfeed protection is in place. - -Raspberry Pi configuration (enable UART) ---------------------------------------- -Edit `/boot/firmware/config.txt` (newer Raspberry Pi OS) or `/boot/config.txt` (older) and ensure: - -``` -enable_uart=1 -``` - -If the serial console/login is enabled, you may need to disable it (via `sudo raspi-config` -> Interface Options -> Serial) so `/dev/serial0` is free. - -After reboot you should see one of: -- `/dev/serial0` (symlink to the primary UART) -- `/dev/ttyAMA0` or `/dev/ttyS0` depending on Pi model. - -Docker usage on Raspberry Pi ----------------------------- -Expose the UART device inside the container: - -``` -docker run \ - --device /dev/serial0:/dev/serial0 \ - -e PICO_SERIAL_PORT=/dev/serial0 \ - your-image:tag -``` - -If using USB instead of GPIO UART, expose `/dev/ttyACM0` (or appropriate) similarly. - -Single combined app container (frontend + backend) -------------------------------------------------- -The project Dockerfile now builds both the frontend and backend. To run on a Pi with GPIO UART wiring: - -``` -docker build -t schafkop-app . -docker run --rm \ - --device /dev/serial0:/dev/serial0 \ - -e PICO_SERIAL_PORT=/dev/serial0 \ - -p 80:3000 \ - schafkop-app -``` - -For USB Pico: -``` -docker run --rm \ - --device /dev/ttyACM0:/dev/ttyACM0 \ - -e PICO_SERIAL_PORT=/dev/ttyACM0 \ - -p 80:3000 \ - schafkop-app -``` - -If you omit PICO_SERIAL_PORT the backend will attempt auto-detection (vendorId 2e8a or common paths). - -Environment variables (suggested backend behavior): -- `PICO_SERIAL_PORT`: If set, backend uses this path directly. -- `PICO_BAUD`: Override baud rate (default 115200). - -Backend port auto-detection logic (recommended order): -1. If `PICO_SERIAL_PORT` env var exists, use it. -2. Else list serial ports; prefer any with vendorId `2e8a` (Pico USB). -3. Else probe common paths: `/dev/serial0`, `/dev/ttyACM0`, `/dev/ttyAMA0`, Windows `COM` ports (highest matching newly added), macOS `/dev/tty.usbmodem*`. -4. Fallback: error with clear message. - -Contract (high-level) ---------------------- -- Commands to Pico: ASCII text lines terminated by LF ("\\n") or CRLF ("\\r\\n"). -- Events/logs from Pico: ASCII text lines, one event per line. Backend must split on newlines. -- Baud rate: 115200 (firmware uses Serial.begin(115200)). - -Accepted commands (input to Pico) ---------------------------------- -1) STEP - - steps: integer (positive number of micro-steps) - - direction: 1 (forward) or 0 (reverse) - - Example: `STEP 4096 1` - -2) SPEED - - delay_us: integer microseconds between internal step micro-operations - - Example: `SPEED 3000` - -Notes: -- Commands are trimmed for leading/trailing whitespace before parsing. -- Invalid or malformed commands result in an error event emitted by the Pico. - -Events/log lines emitted by Pico (output) ----------------------------------------- -The firmware emits structured lines with prefixes so the backend can parse them easily. - -- `EVENT:RECEIVED ` - - Emitted immediately when a command line is received (before execution). - - Example: `EVENT:RECEIVED STEP 4096 1` - -- `EVENT:COMPLETED STEP ` - - Emitted after a successful STEP command completes. - - Example: `EVENT:COMPLETED STEP 4096 1` - -- `EVENT:COMPLETED SPEED ` - - Emitted after the SPEED command is applied. - -- `EVENT:COMPLETED ERROR: ` - - Emitted when a command fails to parse or run. - - Example: `EVENT:COMPLETED ERROR: malformed STEP command` - -- `STATUS: OK` or `STATUS: ERROR: ` - - Short summary always emitted after processing a command. - -Parsing guidance for backend: -- Read raw bytes, split on `\\n` (handle `\\r\\n`). -- Ignore empty lines. -- Inspect prefixes `EVENT:RECEIVED`, `EVENT:COMPLETED`, `STATUS:` and parse the remainder. - -Backend responsibilities (TypeScript) ------------------------------------- -- Open the serial port at 115200 baud. -- Provide a small API: - - `sendStep(steps: number, direction: 0|1): Promise` - - `setSpeed(delayUs: number): Promise` - - `onEvent(cb: (evt: {type: string; payload: string}) => void)` -- Send commands as ASCII lines terminated by `\\n`. -- Listen for events; map them to higher-level promises if desired. -- Implement reconnect logic with exponential backoff if the device disconnects. -- Add basic validation before sending commands to avoid malformed requests. - -Suggested libraries / implementation notes ------------------------------------------ -- Use `serialport` (npm) and its `ReadlineParser` or equivalent. -- Keep a small FIFO of pending commands if you want to wait for `EVENT:COMPLETED` per command. -- For each command you can: - 1. Write the line `CMD\\n` to the port. - 2. Wait for `EVENT:RECEIVED CMD` then `EVENT:COMPLETED ...` and `STATUS: ...`. -- Ensure the backend tolerates duplicate or out-of-order messages (don't assume perfect timing). - -Examples --------- -Example sequence for a STEP command: - -``` -// backend writes: -STEP 4096 1 - -// pico emits: -EVENT:RECEIVED STEP 4096 1 -EVENT:COMPLETED STEP 4096 1 -STATUS: OK -``` - -TypeScript example (auto-detect & simple API) --------------------------------------------- -```ts -import { SerialPort } from 'serialport'; -import { ReadlineParser } from '@serialport/parser-readline'; - -interface PicoEvent { type: string; payload: string; raw: string; } - -async function findPort(): Promise { - if (process.env.PICO_SERIAL_PORT) return process.env.PICO_SERIAL_PORT; - const ports = await SerialPort.list(); - // Prefer Pico USB (vendorId 2e8a) - const pico = ports.find(p => (p.vendorId||'').toLowerCase()==='2e8a'); - if (pico && pico.path) return pico.path; - const candidates = ['/dev/serial0','/dev/ttyACM0','/dev/ttyAMA0']; - for (const c of candidates) if (ports.find(p=>p.path===c)) return c; - if (ports[0]) return ports[0].path; // fallback - throw new Error('No serial ports detected for Pico'); -} - -export class PicoClient { - private port!: SerialPort; - private parser!: ReadlineParser; - private listeners: ((e:PicoEvent)=>void)[] = []; - constructor(private baud = Number(process.env.PICO_BAUD)||115200) {} - async init() { - const path = await findPort(); - this.port = new SerialPort({ path, baudRate: this.baud }); - this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' })); - this.parser.on('data', line => { - const l = line.trim(); - if (!l) return; - let type='RAW'; let payload=l; - if (l.startsWith('EVENT:RECEIVED ')) { type='EVENT:RECEIVED'; payload=l.substring(15); } - else if (l.startsWith('EVENT:COMPLETED ')) { type='EVENT:COMPLETED'; payload=l.substring(17); } - else if (l.startsWith('STATUS: ')) { type='STATUS'; payload=l.substring(8); } - this.listeners.forEach(cb=>cb({ type, payload, raw:l })); - }); - return this; - } - onEvent(cb:(e:PicoEvent)=>void){ this.listeners.push(cb); } - private write(cmd:string){ this.port.write(cmd.endsWith('\n')?cmd:cmd+'\n'); } - sendStep(steps:number, dir:0|1){ this.write(`STEP ${steps} ${dir}`); } - setSpeed(delayUs:number){ this.write(`SPEED ${delayUs}`); } -} - -// Usage example -// (async () => { -// const pico = await new PicoClient().init(); -// pico.onEvent(e => console.log('EVENT', e)); -// pico.setSpeed(3000); -// pico.sendStep(4096,1); -// })(); -``` - -Manual testing --------------- -- Build and upload firmware with PlatformIO (pico environment). -- Open a serial terminal at 115200 and send commands. -- Observe `EVENT:` and `STATUS:` lines. - -Testing inside Docker (Pi UART example) --------------------------------------- -1. Connect wiring (see table above) and power Pico. -2. Confirm `/dev/serial0` exists on host (`ls -l /dev/serial0`). -3. Run container with device passed through. -4. Inside container: run a small Node script using the TypeScript example (compiled) or `screen /dev/serial0 115200` for manual check. - -Troubleshooting ---------------- -| Symptom | Likely Cause | Action | -|---------|--------------|--------| -| No output on UART | Pi serial console still enabled | Disable login shell on serial via `raspi-config` | -| Garbled characters | Baud mismatch | Ensure both sides at 115200 | -| Only EVENT:START appears | Backend not sending newline | Ensure commands end with `\n` | -| USB port not found | Missing udev permissions | Add user to `dialout` (Linux) or run with proper permissions | -| Command times out | Long step count | Consider splitting into smaller STEP commands | - -Extending the firmware ----------------------- -- Add new command parsing in `src/main.cpp`. -- Emit `EVENT:RECEIVED ` when a command is received. -- Emit `EVENT:COMPLETED <...>` once the command finishes (or `EVENT:COMPLETED ERROR: ...` on failure). -- Update this README with any new event formats. - -Files of interest ------------------ -- `src/main.cpp` — serial parser, command dispatch, and event emission. -- `src/ULN2003Stepper.h`, `src/Stepper.h` — stepper implementation. -- `src/schafkopf-bot.cpp` — helper logic; references globals via `extern`. - -Notes for a Copilot implementer ------------------------------- -- Preserve the exact prefixes (`EVENT:RECEIVED`, `EVENT:COMPLETED`, `STATUS:`). -- Add unit tests for the backend parser to assert the event shapes. -- Keep CLI/manual examples minimal and copyable. - - When adding new commands, document: syntax, EVENT:COMPLETED form, error cases, sample sequence. - - Keep the README as the single source of truth for the serial protocol. - ---- -This README is intended as the definitive reference for implementing the TypeScript backend and for extending the Pico firmware. Follow the message formats exactly to avoid breaking existing parsers. diff --git a/pico/include/README b/pico/include/README deleted file mode 100644 index 49819c0..0000000 --- a/pico/include/README +++ /dev/null @@ -1,37 +0,0 @@ - -This directory is intended for project header files. - -A header file is a file containing C declarations and macro definitions -to be shared between several project source files. You request the use of a -header file in your project source file (C, C++, etc) located in `src` folder -by including it, with the C preprocessing directive `#include'. - -```src/main.c - -#include "header.h" - -int main (void) -{ - ... -} -``` - -Including a header file produces the same results as copying the header file -into each source file that needs it. Such copying would be time-consuming -and error-prone. With a header file, the related declarations appear -in only one place. If they need to be changed, they can be changed in one -place, and programs that include the header file will automatically use the -new version when next recompiled. The header file eliminates the labor of -finding and changing all the copies as well as the risk that a failure to -find one copy will result in inconsistencies within a program. - -In C, the convention is to give header files names that end with `.h'. - -Read more about using header files in official GCC documentation: - -* Include Syntax -* Include Operation -* Once-Only Headers -* Computed Includes - -https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/pico/lib/README b/pico/lib/README deleted file mode 100644 index 9379397..0000000 --- a/pico/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -This directory is intended for project specific (private) libraries. -PlatformIO will compile them to static libraries and link into the executable file. - -The source code of each library should be placed in a separate directory -("lib/your_library_name/[Code]"). - -For example, see the structure of the following example libraries `Foo` and `Bar`: - -|--lib -| | -| |--Bar -| | |--docs -| | |--examples -| | |--src -| | |- Bar.c -| | |- Bar.h -| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html -| | -| |--Foo -| | |- Foo.c -| | |- Foo.h -| | -| |- README --> THIS FILE -| -|- platformio.ini -|--src - |- main.c - -Example contents of `src/main.c` using Foo and Bar: -``` -#include -#include - -int main (void) -{ - ... -} - -``` - -The PlatformIO Library Dependency Finder will find automatically dependent -libraries by scanning project source files. - -More information about PlatformIO Library Dependency Finder -- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/pico/lib/Stepper/Stepper.h b/pico/lib/Stepper/Stepper.h new file mode 100644 index 0000000..a4643b8 --- /dev/null +++ b/pico/lib/Stepper/Stepper.h @@ -0,0 +1,22 @@ +#pragma once +class Stepper { +protected: + int steps_per_rev; + virtual void step(int steps, bool direction) = 0; + +public: + void step_rev(double revs, bool direction){ + if(revs == 0.0) return; + if(revs < 0.0){ + direction = !direction; + revs = -revs; + } + + long total_steps = (long)(revs * steps_per_rev + 0.5); + if(total_steps <= 0) return; + + step((int)total_steps, direction); + } + + virtual ~Stepper() {} +}; diff --git a/pico/src/ULN2003Stepper.h b/pico/lib/Stepper/ULN2003Stepper.h similarity index 51% rename from pico/src/ULN2003Stepper.h rename to pico/lib/Stepper/ULN2003Stepper.h index e60b0dc..fe28d1a 100644 --- a/pico/src/ULN2003Stepper.h +++ b/pico/lib/Stepper/ULN2003Stepper.h @@ -1,10 +1,13 @@ #pragma once + #include #include "Stepper.h" #include + class ULN2003Stepper : public Stepper { private: std::array pins; + static constexpr int steps_per_seq = 8; const uint8_t sequence[8][4] = { {1, 0, 0, 0}, @@ -16,24 +19,40 @@ private: {0, 0, 0, 1}, {1, 0, 0, 1} }; + int step_delay_us = 5000; + int current_seq_idx = 0; + + void step(int steps, bool direction) override { + if (steps <= 0) return; + + for (int i = 0; i < steps; ++i) { + // calculate next step index + current_seq_idx = direction + ? (current_seq_idx + 1) % steps_per_seq + : (current_seq_idx - 1 + steps_per_seq) % steps_per_seq; + + // set pins according to the current sequence + digitalWrite(pins[0], sequence[current_seq_idx][0]); + digitalWrite(pins[1], sequence[current_seq_idx][1]); + digitalWrite(pins[2], sequence[current_seq_idx][2]); + digitalWrite(pins[3], sequence[current_seq_idx][3]); + + // wait for the specified delay + delayMicroseconds(step_delay_us); + } + } + public: ULN2003Stepper(std::array pins, int rev_steps) : pins(pins) { steps_per_rev = rev_steps; + for (auto pin : pins) { pinMode(pin, OUTPUT); } } - void step(int steps, bool direction) override { - for (int i = 0; i < steps; ++i) { - int seq_idx = direction ? (i % steps_per_seq) : (steps_per_seq - 1 - (i % steps_per_seq)); - for (int j = 0; j < 4; ++j) { - digitalWrite(pins[j], sequence[seq_idx][j]); - } - delayMicroseconds(step_delay_us); - } - } - void setSpeed(int delay_us) { - step_delay_us = delay_us; - } + + void setStepDelay(int delay_us) { step_delay_us = delay_us; } + + void resetPhase() { current_seq_idx = 0; } }; diff --git a/pico/src/Stepper.h b/pico/src/Stepper.h deleted file mode 100644 index 66284e8..0000000 --- a/pico/src/Stepper.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -// ...existing code... -class Stepper { -protected: - int steps_per_rev; -public: - virtual void step(int steps, bool direction) = 0; - void step_rev(int revs, bool direction){ - step(steps_per_rev*revs, direction); - } - int get_steps_per_rev(){ - return steps_per_rev; - } - virtual ~Stepper() {} -}; diff --git a/pico/src/main.cpp b/pico/src/main.cpp index a7a995b..afe9be5 100644 --- a/pico/src/main.cpp +++ b/pico/src/main.cpp @@ -1,6 +1,6 @@ #include -#include "Stepper.h" -#include "ULN2003Stepper.h" +#include +#include #ifndef LED_BUILTIN #define LED_BUILTIN 25 @@ -16,12 +16,10 @@ public: Serial.begin(baud); - int revSteps = driver1.get_steps_per_rev(); - Serial.print("EVENT:START STEPS_PER_REV "); - Serial.println(revSteps); - waitForHost(1500); - logStartup(); + + // Startup indication + for(int i=0;i<3;i++) blink(60,100); } void loop() { @@ -31,7 +29,11 @@ public: private: String inputBuffer; - enum class CmdType { PING, STEP, SPEED, UNKNOWN }; + struct CommandHandler { + const char* name; + void (SchafkopfSerialApp::*fn)(const String &args); + }; + static const CommandHandler commandTable[]; // defined after class void waitForHost(unsigned long timeoutMs){ unsigned long start = millis(); @@ -45,15 +47,10 @@ private: if(offMs) delay(offMs); } - 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=='\r') continue; if(c=='\n') { processLine(inputBuffer); inputBuffer = ""; @@ -66,17 +63,21 @@ private: 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); + + cmdToken.toUpperCase(); + dispatchCommand(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(""); @@ -85,67 +86,68 @@ private: 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 dispatchCommand(const String &uppercaseToken, const String &args){ + for(size_t i=0; commandTable[i].name != nullptr; ++i){ + if(uppercaseToken.equals(commandTable[i].name)){ + (this->*commandTable[i].fn)(args); + return; + } + } + Serial.print(F("ERR:UNKNOWN COMMAND '")); Serial.print(uppercaseToken); Serial.println("'"); + blink(20); } - 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; } + // ---- COMMANDS ---- + void cmdHealthcheck(const String &){ + Serial.println(F("OK")); + blink(); + } + + void cmdStep(const String &args){ + double 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_rev(steps, dir!=0); + blink(60); + } else { + Serial.println(F("ERR:STEP usage STEP <0|1>")); + blink(20); } } - // Helpers for parsing arguments - bool parseStepArgs(const String &args, int &steps, int &dir){ + + void cmdSpeed(const String &args){ + int delayUs = args.toInt(); + if(delayUs > 0) { + driver1.setStepDelay(delayUs); + Serial.print(F("SPEED: set delay_us=")); Serial.println(delayUs); + blink(); + } else { + Serial.println(F("ERR:SPEED usage SPEED ")); + blink(20); + } + } + + bool parseStepArgs(const String &args, double &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(); + steps = a.toDouble(); dir = b.toInt(); if(steps <= 0) return false; if(!(dir==0 || dir==1)) return false; return true; } }; +// ---- COMMAND TABLE ---- +const SchafkopfSerialApp::CommandHandler SchafkopfSerialApp::commandTable[] = { + {"HEALTHCHECK", &SchafkopfSerialApp::cmdHealthcheck}, + {"STEP", &SchafkopfSerialApp::cmdStep}, + {"SPEED", &SchafkopfSerialApp::cmdSpeed}, + {nullptr, nullptr} +}; + SchafkopfSerialApp app; void setup(){ app.begin(); } diff --git a/pico/test/README b/pico/test/README deleted file mode 100644 index 9b1e87b..0000000 --- a/pico/test/README +++ /dev/null @@ -1,11 +0,0 @@ - -This directory is intended for PlatformIO Test Runner and project tests. - -Unit Testing is a software testing method by which individual units of -source code, sets of one or more MCU program modules together with associated -control data, usage procedures, and operating procedures, are tested to -determine whether they are fit for use. Unit testing finds problems early -in the development cycle. - -More information about PlatformIO Unit Testing: -- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html