mirror of
https://github.com/Vale54321/schafkop-neu.git
synced 2025-12-13 02:29:33 +01:00
refactor: structure pico project
This commit is contained in:
@@ -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> <direction>
|
|
||||||
- steps: integer (positive number of micro-steps)
|
|
||||||
- direction: 1 (forward) or 0 (reverse)
|
|
||||||
- Example: `STEP 4096 1`
|
|
||||||
|
|
||||||
2) SPEED <delay_us>
|
|
||||||
- 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 <raw-command>`
|
|
||||||
- Emitted immediately when a command line is received (before execution).
|
|
||||||
- Example: `EVENT:RECEIVED STEP 4096 1`
|
|
||||||
|
|
||||||
- `EVENT:COMPLETED STEP <steps> <direction>`
|
|
||||||
- Emitted after a successful STEP command completes.
|
|
||||||
- Example: `EVENT:COMPLETED STEP 4096 1`
|
|
||||||
|
|
||||||
- `EVENT:COMPLETED SPEED <delay_us>`
|
|
||||||
- Emitted after the SPEED command is applied.
|
|
||||||
|
|
||||||
- `EVENT:COMPLETED ERROR: <message>`
|
|
||||||
- Emitted when a command fails to parse or run.
|
|
||||||
- Example: `EVENT:COMPLETED ERROR: malformed STEP command`
|
|
||||||
|
|
||||||
- `STATUS: OK` or `STATUS: ERROR: <message>`
|
|
||||||
- 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<void>`
|
|
||||||
- `setSpeed(delayUs: number): Promise<void>`
|
|
||||||
- `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<string> {
|
|
||||||
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 <cmd>` 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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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 <Foo.h>
|
|
||||||
#include <Bar.h>
|
|
||||||
|
|
||||||
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
|
|
||||||
22
pico/lib/Stepper/Stepper.h
Normal file
22
pico/lib/Stepper/Stepper.h
Normal file
@@ -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() {}
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "Stepper.h"
|
#include "Stepper.h"
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
class ULN2003Stepper : public Stepper {
|
class ULN2003Stepper : public Stepper {
|
||||||
private:
|
private:
|
||||||
std::array<uint8_t, 4> pins;
|
std::array<uint8_t, 4> pins;
|
||||||
|
|
||||||
static constexpr int steps_per_seq = 8;
|
static constexpr int steps_per_seq = 8;
|
||||||
const uint8_t sequence[8][4] = {
|
const uint8_t sequence[8][4] = {
|
||||||
{1, 0, 0, 0},
|
{1, 0, 0, 0},
|
||||||
@@ -16,24 +19,40 @@ private:
|
|||||||
{0, 0, 0, 1},
|
{0, 0, 0, 1},
|
||||||
{1, 0, 0, 1}
|
{1, 0, 0, 1}
|
||||||
};
|
};
|
||||||
|
|
||||||
int step_delay_us = 5000;
|
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:
|
public:
|
||||||
ULN2003Stepper(std::array<uint8_t, 4> pins, int rev_steps) : pins(pins) {
|
ULN2003Stepper(std::array<uint8_t, 4> pins, int rev_steps) : pins(pins) {
|
||||||
steps_per_rev = rev_steps;
|
steps_per_rev = rev_steps;
|
||||||
|
|
||||||
for (auto pin : pins) {
|
for (auto pin : pins) {
|
||||||
pinMode(pin, OUTPUT);
|
pinMode(pin, OUTPUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void step(int steps, bool direction) override {
|
|
||||||
for (int i = 0; i < steps; ++i) {
|
void setStepDelay(int delay_us) { step_delay_us = delay_us; }
|
||||||
int seq_idx = direction ? (i % steps_per_seq) : (steps_per_seq - 1 - (i % steps_per_seq));
|
|
||||||
for (int j = 0; j < 4; ++j) {
|
void resetPhase() { current_seq_idx = 0; }
|
||||||
digitalWrite(pins[j], sequence[seq_idx][j]);
|
|
||||||
}
|
|
||||||
delayMicroseconds(step_delay_us);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void setSpeed(int delay_us) {
|
|
||||||
step_delay_us = delay_us;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -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() {}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "Stepper.h"
|
#include <Stepper.h>
|
||||||
#include "ULN2003Stepper.h"
|
#include <ULN2003Stepper.h>
|
||||||
|
|
||||||
#ifndef LED_BUILTIN
|
#ifndef LED_BUILTIN
|
||||||
#define LED_BUILTIN 25
|
#define LED_BUILTIN 25
|
||||||
@@ -16,12 +16,10 @@ public:
|
|||||||
|
|
||||||
Serial.begin(baud);
|
Serial.begin(baud);
|
||||||
|
|
||||||
int revSteps = driver1.get_steps_per_rev();
|
|
||||||
Serial.print("EVENT:START STEPS_PER_REV ");
|
|
||||||
Serial.println(revSteps);
|
|
||||||
|
|
||||||
waitForHost(1500);
|
waitForHost(1500);
|
||||||
logStartup();
|
|
||||||
|
// Startup indication
|
||||||
|
for(int i=0;i<3;i++) blink(60,100);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
@@ -31,7 +29,11 @@ public:
|
|||||||
private:
|
private:
|
||||||
String inputBuffer;
|
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){
|
void waitForHost(unsigned long timeoutMs){
|
||||||
unsigned long start = millis();
|
unsigned long start = millis();
|
||||||
@@ -45,15 +47,10 @@ private:
|
|||||||
if(offMs) delay(offMs);
|
if(offMs) delay(offMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void logStartup(){
|
|
||||||
Serial.println(F("HELLO START"));
|
|
||||||
for(int i=0;i<3;i++) blink(60,100);
|
|
||||||
}
|
|
||||||
|
|
||||||
void pollSerial(){
|
void pollSerial(){
|
||||||
while(Serial.available()){
|
while(Serial.available()){
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if(c=='\r') continue; // ignore CR
|
if(c=='\r') continue;
|
||||||
if(c=='\n') {
|
if(c=='\n') {
|
||||||
processLine(inputBuffer);
|
processLine(inputBuffer);
|
||||||
inputBuffer = "";
|
inputBuffer = "";
|
||||||
@@ -66,17 +63,21 @@ private:
|
|||||||
void processLine(const String &raw){
|
void processLine(const String &raw){
|
||||||
String line = raw;
|
String line = raw;
|
||||||
line.trim();
|
line.trim();
|
||||||
|
|
||||||
if(!line.length()) return;
|
if(!line.length()) return;
|
||||||
|
|
||||||
String cmdToken = firstToken(line);
|
String cmdToken = firstToken(line);
|
||||||
String rest = remainingAfterFirst(line);
|
String rest = remainingAfterFirst(line);
|
||||||
CmdType type = classify(cmdToken);
|
|
||||||
handleCommand(type, cmdToken, rest);
|
cmdToken.toUpperCase();
|
||||||
|
dispatchCommand(cmdToken, rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String firstToken(const String &line){
|
static String firstToken(const String &line){
|
||||||
int sp = line.indexOf(' ');
|
int sp = line.indexOf(' ');
|
||||||
return (sp==-1)? line : line.substring(0, sp);
|
return (sp==-1)? line : line.substring(0, sp);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String remainingAfterFirst(const String &line){
|
static String remainingAfterFirst(const String &line){
|
||||||
int sp = line.indexOf(' ');
|
int sp = line.indexOf(' ');
|
||||||
if (sp==-1) return String("");
|
if (sp==-1) return String("");
|
||||||
@@ -85,67 +86,68 @@ private:
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
CmdType classify(String token){
|
void dispatchCommand(const String &uppercaseToken, const String &args){
|
||||||
token.toUpperCase();
|
for(size_t i=0; commandTable[i].name != nullptr; ++i){
|
||||||
if(token==F("PING")) return CmdType::PING;
|
if(uppercaseToken.equals(commandTable[i].name)){
|
||||||
if(token==F("STEP")) return CmdType::STEP;
|
(this->*commandTable[i].fn)(args);
|
||||||
if(token==F("SPEED")) return CmdType::SPEED;
|
return;
|
||||||
return CmdType::UNKNOWN;
|
}
|
||||||
|
}
|
||||||
|
Serial.print(F("ERR:UNKNOWN COMMAND '")); Serial.print(uppercaseToken); Serial.println("'");
|
||||||
|
blink(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleCommand(CmdType type, const String &token, const String &args){
|
// ---- COMMANDS ----
|
||||||
switch(type){
|
void cmdHealthcheck(const String &){
|
||||||
case CmdType::PING: {
|
Serial.println(F("OK"));
|
||||||
Serial.println(F("PONG"));
|
blink();
|
||||||
blink();
|
}
|
||||||
break; }
|
|
||||||
case CmdType::STEP: {
|
void cmdStep(const String &args){
|
||||||
// Expected: STEP <steps> <dir>
|
double steps = -1; int dir = -1;
|
||||||
int steps = -1; int dir = -1;
|
if(parseStepArgs(args, steps, dir)) {
|
||||||
if(parseStepArgs(args, steps, dir)) {
|
Serial.print(F("STEP: moving ")); Serial.print(steps); Serial.print(F(" dir=")); Serial.println(dir);
|
||||||
Serial.print(F("STEP: moving ")); Serial.print(steps); Serial.print(F(" dir=")); Serial.println(dir);
|
driver1.step_rev(steps, dir!=0);
|
||||||
driver1.step(steps, dir!=0);
|
blink(60);
|
||||||
blink(60);
|
} else {
|
||||||
} else {
|
Serial.println(F("ERR:STEP usage STEP <revs> <0|1>"));
|
||||||
Serial.println(F("ERR:STEP usage STEP <steps> <0|1>"));
|
blink(20);
|
||||||
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){
|
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 <positive_delay_us>"));
|
||||||
|
blink(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseStepArgs(const String &args, double &steps, int &dir){
|
||||||
int sp = args.indexOf(' ');
|
int sp = args.indexOf(' ');
|
||||||
if(sp == -1) return false;
|
if(sp == -1) return false;
|
||||||
String a = args.substring(0, sp); a.trim();
|
String a = args.substring(0, sp); a.trim();
|
||||||
String b = args.substring(sp+1); b.trim();
|
String b = args.substring(sp+1); b.trim();
|
||||||
if(!a.length() || !b.length()) return false;
|
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(steps <= 0) return false;
|
||||||
if(!(dir==0 || dir==1)) return false;
|
if(!(dir==0 || dir==1)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- COMMAND TABLE ----
|
||||||
|
const SchafkopfSerialApp::CommandHandler SchafkopfSerialApp::commandTable[] = {
|
||||||
|
{"HEALTHCHECK", &SchafkopfSerialApp::cmdHealthcheck},
|
||||||
|
{"STEP", &SchafkopfSerialApp::cmdStep},
|
||||||
|
{"SPEED", &SchafkopfSerialApp::cmdSpeed},
|
||||||
|
{nullptr, nullptr}
|
||||||
|
};
|
||||||
|
|
||||||
SchafkopfSerialApp app;
|
SchafkopfSerialApp app;
|
||||||
|
|
||||||
void setup(){ app.begin(); }
|
void setup(){ app.begin(); }
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user