Merge pull request 'firmware_deploy' (#3) from firmware_deploy into master
Reviewed-on: #3
This commit is contained in:
72
.gitea/workflows/deploy-ftp.yml
Normal file
72
.gitea/workflows/deploy-ftp.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
name: Deploy firmware via FTP (main)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-main
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Build and FTP Sync
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Install PlatformIO
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install platformio
|
||||||
|
|
||||||
|
- name: Build firmware bundle
|
||||||
|
run: pio run -e uno
|
||||||
|
|
||||||
|
- name: Install lftp client
|
||||||
|
run: |
|
||||||
|
if command -v lftp >/dev/null 2>&1; then
|
||||||
|
echo 'lftp is already available on this runner image'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y lftp
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y lftp
|
||||||
|
|
||||||
|
- name: Upload firmware bundle via lftp
|
||||||
|
env:
|
||||||
|
FTP_SERVER: ${{ secrets.FTP_SERVER }}
|
||||||
|
FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
|
||||||
|
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||||
|
FTP_SERVER_DIR: ${{ secrets.FTP_SERVER_DIR }}
|
||||||
|
run: |
|
||||||
|
lftp <<EOF
|
||||||
|
set cmd:fail-exit true
|
||||||
|
set net:timeout 120
|
||||||
|
set net:max-retries 2
|
||||||
|
set net:reconnect-interval-base 5
|
||||||
|
set ftp:passive-mode true
|
||||||
|
set ftp:prefer-epsv false
|
||||||
|
set ftp:ssl-force true
|
||||||
|
set ftp:ssl-protect-data true
|
||||||
|
set ftp:ssl-auth TLS
|
||||||
|
set ssl:verify-certificate no
|
||||||
|
open -u "$FTP_USERNAME","$FTP_PASSWORD" -p 21 "$FTP_SERVER"
|
||||||
|
mirror -R --verbose --delete \
|
||||||
|
--exclude-glob .DS_Store \
|
||||||
|
firmware/ "$FTP_SERVER_DIR"
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,6 +15,8 @@ local.properties
|
|||||||
.classpath
|
.classpath
|
||||||
.settings/
|
.settings/
|
||||||
.loadpath
|
.loadpath
|
||||||
|
.vscode
|
||||||
|
.pio
|
||||||
|
|
||||||
# External tool builders
|
# External tool builders
|
||||||
.externalToolBuilders/
|
.externalToolBuilders/
|
||||||
@@ -213,3 +215,4 @@ pip-log.txt
|
|||||||
|
|
||||||
#Mr Developer
|
#Mr Developer
|
||||||
.mr.developer.cfg
|
.mr.developer.cfg
|
||||||
|
src/credentials.h
|
||||||
|
|||||||
63
AGENTS.md
Normal file
63
AGENTS.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This repository contains EggDuino firmware (EggBot protocol emulation) for an ESP32 build target using PlatformIO.
|
||||||
|
Use this file as the working contract for code agents in this repo.
|
||||||
|
|
||||||
|
## Repository Map
|
||||||
|
- `src/main.cpp`: firmware entrypoint (`setup`/`loop`), serial command processing, optional button/test toggles.
|
||||||
|
- `src/Functions.cpp`: EggBot command handlers (`SM`, `SP`, `EM`, `SC`, `QP`, etc.) and command registration.
|
||||||
|
- `src/Helper_Functions.cpp`: hardware init, motor/servo control, move preparation, ACK/error responses.
|
||||||
|
- `src/Config_Web.cpp`: optional Wi-Fi web UI and JSON config persistence via SPIFFS.
|
||||||
|
- `src/Logging.cpp`: ring-buffer log collection and `/api/logs` JSON serialization.
|
||||||
|
- `include/EggDuino.h`: global declarations, pin/microstep constants, cross-file API.
|
||||||
|
- `lib/SerialCommand/src/*`: local serial command parser implementation.
|
||||||
|
- `platformio.ini`: single build environment (`[env:uno]`) that actually targets `esp32dev`.
|
||||||
|
|
||||||
|
## Build, Flash, Monitor
|
||||||
|
- Build: `~/.platformio/penv/bin/pio run -e uno`
|
||||||
|
- Upload: `~/.platformio/penv/bin/pio run -e uno -t upload`
|
||||||
|
- Serial monitor: `~/.platformio/penv/bin/pio device monitor -b 115200`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `platformio.ini` currently pins `upload_port = /dev/tty.usbserial-110`; treat this as machine-local and change only when required.
|
||||||
|
- There are no unit tests in this repository; a successful PlatformIO build is the minimum validation.
|
||||||
|
|
||||||
|
## Local Config And Secrets
|
||||||
|
- Wi-Fi config is provided by `src/credentials.h`.
|
||||||
|
- Keep `src/credentials.h` local-only; `.gitignore` already excludes it.
|
||||||
|
- For default behavior (no Wi-Fi web server), keep:
|
||||||
|
- `const char *kWifiSsid = 0;`
|
||||||
|
- `const char *kWifiPassword = 0;`
|
||||||
|
|
||||||
|
## Agent Rules For Code Changes
|
||||||
|
1. Preserve EggBot serial protocol behavior.
|
||||||
|
- Commands are wired in `makeComInterface()`.
|
||||||
|
- ACK/error responses are protocol-significant (`"OK\\r\\n"` and `"unknown CMD\\r\\n"`).
|
||||||
|
2. Keep movement math semantics intact.
|
||||||
|
- `prepareMove()` handles microstep correction and step error accumulation (`g_iRotStepError`, `g_iPenStepError`).
|
||||||
|
- Do not replace this with naive float-heavy logic unless explicitly requested.
|
||||||
|
3. Respect real-time loop constraints.
|
||||||
|
- `loop()` must remain non-blocking except for protocol-required waits.
|
||||||
|
- Avoid adding expensive dynamic allocation in high-frequency paths.
|
||||||
|
4. Keep hardware mapping centralized.
|
||||||
|
- Pin and microstep constants belong in `include/EggDuino.h`.
|
||||||
|
- If changing pinouts, update only there and keep ESP32/non-ESP32 guards coherent.
|
||||||
|
5. Do not hand-edit generated IDE metadata.
|
||||||
|
- `.vscode/c_cpp_properties.json` and `.vscode/launch.json` are generated by PlatformIO.
|
||||||
|
- Prefer edits in `platformio.ini` instead.
|
||||||
|
6. Keep edits minimal and targeted.
|
||||||
|
- Match existing style (global `g_*` variables, Arduino types, simple free functions).
|
||||||
|
- Avoid broad refactors unless explicitly requested.
|
||||||
|
|
||||||
|
## When Adding Config Parameters To Web UI
|
||||||
|
`Config_Web.cpp` currently has an empty `configParameters[]` array. To add persisted parameters:
|
||||||
|
1. Add the backing global variable (typically in existing global state).
|
||||||
|
2. Add a `ConfigParameter` entry to `configParameters[]` with key, pointer, description, default.
|
||||||
|
3. Ensure defaults are safe in `applyDefaults()` flow.
|
||||||
|
4. Rebuild and verify `GET/POST /api/config` still round-trips.
|
||||||
|
|
||||||
|
## Definition Of Done For Agents
|
||||||
|
Before finishing, run:
|
||||||
|
1. `~/.platformio/penv/bin/pio run -e uno`
|
||||||
|
2. Summarize changed files and any behavior-impacting protocol/hardware implications.
|
||||||
633
AccelStepper.cpp
633
AccelStepper.cpp
@@ -1,633 +0,0 @@
|
|||||||
// AccelStepper.cpp
|
|
||||||
//
|
|
||||||
// Copyright (C) 2009-2013 Mike McCauley
|
|
||||||
// $Id: AccelStepper.cpp,v 1.19 2014/10/31 06:05:27 mikem Exp mikem $
|
|
||||||
|
|
||||||
#include "AccelStepper.h"
|
|
||||||
|
|
||||||
#if 0
|
|
||||||
// Some debugging assistance
|
|
||||||
void dump(uint8_t* p, int l)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; i < l; i++)
|
|
||||||
{
|
|
||||||
Serial.print(p[i], HEX);
|
|
||||||
Serial.print(" ");
|
|
||||||
}
|
|
||||||
Serial.println("");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void AccelStepper::moveTo(long absolute)
|
|
||||||
{
|
|
||||||
if (_targetPos != absolute)
|
|
||||||
{
|
|
||||||
_targetPos = absolute;
|
|
||||||
computeNewSpeed();
|
|
||||||
// compute new n?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::move(long relative)
|
|
||||||
{
|
|
||||||
moveTo(_currentPos + relative);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements steps according to the current step interval
|
|
||||||
// You must call this at least once per step
|
|
||||||
// returns true if a step occurred
|
|
||||||
boolean AccelStepper::runSpeed()
|
|
||||||
{
|
|
||||||
// Dont do anything unless we actually have a step interval
|
|
||||||
if (!_stepInterval)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
unsigned long time = micros();
|
|
||||||
unsigned long nextStepTime = _lastStepTime + _stepInterval;
|
|
||||||
// Gymnastics to detect wrapping of either the nextStepTime and/or the current time
|
|
||||||
if ( ((nextStepTime >= _lastStepTime) && ((time >= nextStepTime) || (time < _lastStepTime)))
|
|
||||||
|| ((nextStepTime < _lastStepTime) && ((time >= nextStepTime) && (time < _lastStepTime))))
|
|
||||||
{
|
|
||||||
if (_direction == DIRECTION_CW)
|
|
||||||
{
|
|
||||||
// Clockwise
|
|
||||||
_currentPos += 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Anticlockwise
|
|
||||||
_currentPos -= 1;
|
|
||||||
}
|
|
||||||
step(_currentPos);
|
|
||||||
|
|
||||||
_lastStepTime = time;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
long AccelStepper::distanceToGo()
|
|
||||||
{
|
|
||||||
return _targetPos - _currentPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
long AccelStepper::targetPosition()
|
|
||||||
{
|
|
||||||
return _targetPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
long AccelStepper::currentPosition()
|
|
||||||
{
|
|
||||||
return _currentPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Useful during initialisations or after initial positioning
|
|
||||||
// Sets speed to 0
|
|
||||||
void AccelStepper::setCurrentPosition(long position)
|
|
||||||
{
|
|
||||||
_targetPos = _currentPos = position;
|
|
||||||
_n = 0;
|
|
||||||
_stepInterval = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::computeNewSpeed()
|
|
||||||
{
|
|
||||||
long distanceTo = distanceToGo(); // +ve is clockwise from curent location
|
|
||||||
|
|
||||||
long stepsToStop = (long)((_speed * _speed) / (2.0 * _acceleration)); // Equation 16
|
|
||||||
|
|
||||||
if (distanceTo == 0 && stepsToStop <= 1)
|
|
||||||
{
|
|
||||||
// We are at the target and its time to stop
|
|
||||||
_stepInterval = 0;
|
|
||||||
_speed = 0.0;
|
|
||||||
_n = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distanceTo > 0)
|
|
||||||
{
|
|
||||||
// We are anticlockwise from the target
|
|
||||||
// Need to go clockwise from here, maybe decelerate now
|
|
||||||
if (_n > 0)
|
|
||||||
{
|
|
||||||
// Currently accelerating, need to decel now? Or maybe going the wrong way?
|
|
||||||
if ((stepsToStop >= distanceTo) || _direction == DIRECTION_CCW)
|
|
||||||
_n = -stepsToStop; // Start deceleration
|
|
||||||
}
|
|
||||||
else if (_n < 0)
|
|
||||||
{
|
|
||||||
// Currently decelerating, need to accel again?
|
|
||||||
if ((stepsToStop < distanceTo) && _direction == DIRECTION_CW)
|
|
||||||
_n = -_n; // Start accceleration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (distanceTo < 0)
|
|
||||||
{
|
|
||||||
// We are clockwise from the target
|
|
||||||
// Need to go anticlockwise from here, maybe decelerate
|
|
||||||
if (_n > 0)
|
|
||||||
{
|
|
||||||
// Currently accelerating, need to decel now? Or maybe going the wrong way?
|
|
||||||
if ((stepsToStop >= -distanceTo) || _direction == DIRECTION_CW)
|
|
||||||
_n = -stepsToStop; // Start deceleration
|
|
||||||
}
|
|
||||||
else if (_n < 0)
|
|
||||||
{
|
|
||||||
// Currently decelerating, need to accel again?
|
|
||||||
if ((stepsToStop < -distanceTo) && _direction == DIRECTION_CCW)
|
|
||||||
_n = -_n; // Start accceleration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to accelerate or decelerate
|
|
||||||
if (_n == 0)
|
|
||||||
{
|
|
||||||
// First step from stopped
|
|
||||||
_cn = _c0;
|
|
||||||
_direction = (distanceTo > 0) ? DIRECTION_CW : DIRECTION_CCW;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Subsequent step. Works for accel (n is +_ve) and decel (n is -ve).
|
|
||||||
_cn = _cn - ((2.0 * _cn) / ((4.0 * _n) + 1)); // Equation 13
|
|
||||||
_cn = max(_cn, _cmin);
|
|
||||||
}
|
|
||||||
_n++;
|
|
||||||
_stepInterval = _cn;
|
|
||||||
_speed = 1000000.0 / _cn;
|
|
||||||
if (_direction == DIRECTION_CCW)
|
|
||||||
_speed = -_speed;
|
|
||||||
|
|
||||||
#if 0
|
|
||||||
Serial.println(_speed);
|
|
||||||
Serial.println(_acceleration);
|
|
||||||
Serial.println(_cn);
|
|
||||||
Serial.println(_c0);
|
|
||||||
Serial.println(_n);
|
|
||||||
Serial.println(_stepInterval);
|
|
||||||
Serial.println(distanceTo);
|
|
||||||
Serial.println(stepsToStop);
|
|
||||||
Serial.println("-----");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the motor to implement speed and acceleration in order to proceed to the target position
|
|
||||||
// You must call this at least once per step, preferably in your main loop
|
|
||||||
// If the motor is in the desired position, the cost is very small
|
|
||||||
// returns true if the motor is still running to the target position.
|
|
||||||
boolean AccelStepper::run()
|
|
||||||
{
|
|
||||||
if (runSpeed())
|
|
||||||
computeNewSpeed();
|
|
||||||
return _speed != 0.0 || distanceToGo() != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
AccelStepper::AccelStepper(uint8_t interface, uint8_t pin1, uint8_t pin2, uint8_t pin3, uint8_t pin4, bool enable)
|
|
||||||
{
|
|
||||||
_interface = interface;
|
|
||||||
_currentPos = 0;
|
|
||||||
_targetPos = 0;
|
|
||||||
_speed = 0.0;
|
|
||||||
_maxSpeed = 1.0;
|
|
||||||
_acceleration = 0.0;
|
|
||||||
_sqrt_twoa = 1.0;
|
|
||||||
_stepInterval = 0;
|
|
||||||
_minPulseWidth = 1;
|
|
||||||
_enablePin = 0xff;
|
|
||||||
_lastStepTime = 0;
|
|
||||||
_pin[0] = pin1;
|
|
||||||
_pin[1] = pin2;
|
|
||||||
_pin[2] = pin3;
|
|
||||||
_pin[3] = pin4;
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
_n = 0;
|
|
||||||
_c0 = 0.0;
|
|
||||||
_cn = 0.0;
|
|
||||||
_cmin = 1.0;
|
|
||||||
_direction = DIRECTION_CCW;
|
|
||||||
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < 4; i++)
|
|
||||||
_pinInverted[i] = 0;
|
|
||||||
if (enable)
|
|
||||||
enableOutputs();
|
|
||||||
// Some reasonable default
|
|
||||||
setAcceleration(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
AccelStepper::AccelStepper(void (*forward)(), void (*backward)())
|
|
||||||
{
|
|
||||||
_interface = 0;
|
|
||||||
_currentPos = 0;
|
|
||||||
_targetPos = 0;
|
|
||||||
_speed = 0.0;
|
|
||||||
_maxSpeed = 1.0;
|
|
||||||
_acceleration = 0.0;
|
|
||||||
_sqrt_twoa = 1.0;
|
|
||||||
_stepInterval = 0;
|
|
||||||
_minPulseWidth = 1;
|
|
||||||
_enablePin = 0xff;
|
|
||||||
_lastStepTime = 0;
|
|
||||||
_pin[0] = 0;
|
|
||||||
_pin[1] = 0;
|
|
||||||
_pin[2] = 0;
|
|
||||||
_pin[3] = 0;
|
|
||||||
_forward = forward;
|
|
||||||
_backward = backward;
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
_n = 0;
|
|
||||||
_c0 = 0.0;
|
|
||||||
_cn = 0.0;
|
|
||||||
_cmin = 1.0;
|
|
||||||
_direction = DIRECTION_CCW;
|
|
||||||
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < 4; i++)
|
|
||||||
_pinInverted[i] = 0;
|
|
||||||
// Some reasonable default
|
|
||||||
setAcceleration(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setMaxSpeed(float speed)
|
|
||||||
{
|
|
||||||
if (_maxSpeed != speed)
|
|
||||||
{
|
|
||||||
_maxSpeed = speed;
|
|
||||||
_cmin = 1000000.0 / speed;
|
|
||||||
// Recompute _n from current speed and adjust speed if accelerating or cruising
|
|
||||||
if (_n > 0)
|
|
||||||
{
|
|
||||||
_n = (long)((_speed * _speed) / (2.0 * _acceleration)); // Equation 16
|
|
||||||
computeNewSpeed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setAcceleration(float acceleration)
|
|
||||||
{
|
|
||||||
if (acceleration == 0.0)
|
|
||||||
return;
|
|
||||||
if (_acceleration != acceleration)
|
|
||||||
{
|
|
||||||
// Recompute _n per Equation 17
|
|
||||||
_n = _n * (_acceleration / acceleration);
|
|
||||||
// New c0 per Equation 7, with correction per Equation 15
|
|
||||||
_c0 = 0.676 * sqrt(2.0 / acceleration) * 1000000.0; // Equation 15
|
|
||||||
_acceleration = acceleration;
|
|
||||||
computeNewSpeed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setSpeed(float speed)
|
|
||||||
{
|
|
||||||
if (speed == _speed)
|
|
||||||
return;
|
|
||||||
speed = constrain(speed, -_maxSpeed, _maxSpeed);
|
|
||||||
if (speed == 0.0)
|
|
||||||
_stepInterval = 0;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_stepInterval = fabs(1000000.0 / speed);
|
|
||||||
_direction = (speed > 0.0) ? DIRECTION_CW : DIRECTION_CCW;
|
|
||||||
}
|
|
||||||
_speed = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
float AccelStepper::speed()
|
|
||||||
{
|
|
||||||
return _speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step(long step)
|
|
||||||
{
|
|
||||||
switch (_interface)
|
|
||||||
{
|
|
||||||
case FUNCTION:
|
|
||||||
step0(step);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DRIVER:
|
|
||||||
step1(step);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FULL2WIRE:
|
|
||||||
step2(step);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FULL3WIRE:
|
|
||||||
step3(step);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FULL4WIRE:
|
|
||||||
step4(step);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HALF3WIRE:
|
|
||||||
step6(step);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HALF4WIRE:
|
|
||||||
step8(step);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// You might want to override this to implement eg serial output
|
|
||||||
// bit 0 of the mask corresponds to _pin[0]
|
|
||||||
// bit 1 of the mask corresponds to _pin[1]
|
|
||||||
// ....
|
|
||||||
void AccelStepper::setOutputPins(uint8_t mask)
|
|
||||||
{
|
|
||||||
uint8_t numpins = 2;
|
|
||||||
if (_interface == FULL4WIRE || _interface == HALF4WIRE)
|
|
||||||
numpins = 4;
|
|
||||||
else if (_interface == FULL3WIRE || _interface == HALF3WIRE)
|
|
||||||
numpins = 3;
|
|
||||||
uint8_t i;
|
|
||||||
for (i = 0; i < numpins; i++)
|
|
||||||
digitalWrite(_pin[i], (mask & (1 << i)) ? (HIGH ^ _pinInverted[i]) : (LOW ^ _pinInverted[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0 pin step function (ie for functional usage)
|
|
||||||
void AccelStepper::step0(long step)
|
|
||||||
{
|
|
||||||
if (_speed > 0)
|
|
||||||
_forward();
|
|
||||||
else
|
|
||||||
_backward();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 pin step function (ie for stepper drivers)
|
|
||||||
// This is passed the current step number (0 to 7)
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step1(long step)
|
|
||||||
{
|
|
||||||
// _pin[0] is step, _pin[1] is direction
|
|
||||||
setOutputPins(_direction ? 0b10 : 0b00); // Set direction first else get rogue pulses
|
|
||||||
setOutputPins(_direction ? 0b11 : 0b01); // step HIGH
|
|
||||||
// Caution 200ns setup time
|
|
||||||
// Delay the minimum allowed pulse width
|
|
||||||
delayMicroseconds(_minPulseWidth);
|
|
||||||
setOutputPins(_direction ? 0b10 : 0b00); // step LOW
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 2 pin step function
|
|
||||||
// This is passed the current step number (0 to 7)
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step2(long step)
|
|
||||||
{
|
|
||||||
switch (step & 0x3)
|
|
||||||
{
|
|
||||||
case 0: /* 01 */
|
|
||||||
setOutputPins(0b10);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: /* 11 */
|
|
||||||
setOutputPins(0b11);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: /* 10 */
|
|
||||||
setOutputPins(0b01);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3: /* 00 */
|
|
||||||
setOutputPins(0b00);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 3 pin step function
|
|
||||||
// This is passed the current step number (0 to 7)
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step3(long step)
|
|
||||||
{
|
|
||||||
switch (step % 3)
|
|
||||||
{
|
|
||||||
case 0: // 100
|
|
||||||
setOutputPins(0b100);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: // 001
|
|
||||||
setOutputPins(0b001);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: //010
|
|
||||||
setOutputPins(0b010);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4 pin step function for half stepper
|
|
||||||
// This is passed the current step number (0 to 7)
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step4(long step)
|
|
||||||
{
|
|
||||||
switch (step & 0x3)
|
|
||||||
{
|
|
||||||
case 0: // 1010
|
|
||||||
setOutputPins(0b0101);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: // 0110
|
|
||||||
setOutputPins(0b0110);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: //0101
|
|
||||||
setOutputPins(0b1010);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3: //1001
|
|
||||||
setOutputPins(0b1001);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3 pin half step function
|
|
||||||
// This is passed the current step number (0 to 7)
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step6(long step)
|
|
||||||
{
|
|
||||||
switch (step % 6)
|
|
||||||
{
|
|
||||||
case 0: // 100
|
|
||||||
setOutputPins(0b100);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: // 101
|
|
||||||
setOutputPins(0b101);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: // 001
|
|
||||||
setOutputPins(0b001);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3: // 011
|
|
||||||
setOutputPins(0b011);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 4: // 010
|
|
||||||
setOutputPins(0b010);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 5: // 011
|
|
||||||
setOutputPins(0b110);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4 pin half step function
|
|
||||||
// This is passed the current step number (0 to 7)
|
|
||||||
// Subclasses can override
|
|
||||||
void AccelStepper::step8(long step)
|
|
||||||
{
|
|
||||||
switch (step & 0x7)
|
|
||||||
{
|
|
||||||
case 0: // 1000
|
|
||||||
setOutputPins(0b0001);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: // 1010
|
|
||||||
setOutputPins(0b0101);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: // 0010
|
|
||||||
setOutputPins(0b0100);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3: // 0110
|
|
||||||
setOutputPins(0b0110);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 4: // 0100
|
|
||||||
setOutputPins(0b0010);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 5: //0101
|
|
||||||
setOutputPins(0b1010);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 6: // 0001
|
|
||||||
setOutputPins(0b1000);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 7: //1001
|
|
||||||
setOutputPins(0b1001);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevents power consumption on the outputs
|
|
||||||
void AccelStepper::disableOutputs()
|
|
||||||
{
|
|
||||||
if (! _interface) return;
|
|
||||||
|
|
||||||
setOutputPins(0); // Handles inversion automatically
|
|
||||||
if (_enablePin != 0xff)
|
|
||||||
digitalWrite(_enablePin, LOW ^ _enableInverted);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::enableOutputs()
|
|
||||||
{
|
|
||||||
if (! _interface)
|
|
||||||
return;
|
|
||||||
|
|
||||||
pinMode(_pin[0], OUTPUT);
|
|
||||||
pinMode(_pin[1], OUTPUT);
|
|
||||||
if (_interface == FULL4WIRE || _interface == HALF4WIRE)
|
|
||||||
{
|
|
||||||
pinMode(_pin[2], OUTPUT);
|
|
||||||
pinMode(_pin[3], OUTPUT);
|
|
||||||
}
|
|
||||||
else if (_interface == FULL3WIRE || _interface == HALF3WIRE)
|
|
||||||
{
|
|
||||||
pinMode(_pin[2], OUTPUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_enablePin != 0xff)
|
|
||||||
{
|
|
||||||
pinMode(_enablePin, OUTPUT);
|
|
||||||
digitalWrite(_enablePin, HIGH ^ _enableInverted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setMinPulseWidth(unsigned int minWidth)
|
|
||||||
{
|
|
||||||
_minPulseWidth = minWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setEnablePin(uint8_t enablePin)
|
|
||||||
{
|
|
||||||
_enablePin = enablePin;
|
|
||||||
|
|
||||||
// This happens after construction, so init pin now.
|
|
||||||
if (_enablePin != 0xff)
|
|
||||||
{
|
|
||||||
pinMode(_enablePin, OUTPUT);
|
|
||||||
digitalWrite(_enablePin, HIGH ^ _enableInverted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setPinsInverted(bool directionInvert, bool stepInvert, bool enableInvert)
|
|
||||||
{
|
|
||||||
_pinInverted[0] = stepInvert;
|
|
||||||
_pinInverted[1] = directionInvert;
|
|
||||||
_enableInverted = enableInvert;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::setPinsInverted(bool pin1Invert, bool pin2Invert, bool pin3Invert, bool pin4Invert, bool enableInvert)
|
|
||||||
{
|
|
||||||
_pinInverted[0] = pin1Invert;
|
|
||||||
_pinInverted[1] = pin2Invert;
|
|
||||||
_pinInverted[2] = pin3Invert;
|
|
||||||
_pinInverted[3] = pin4Invert;
|
|
||||||
_enableInverted = enableInvert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocks until the target position is reached and stopped
|
|
||||||
void AccelStepper::runToPosition()
|
|
||||||
{
|
|
||||||
while (run())
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean AccelStepper::runSpeedToPosition()
|
|
||||||
{
|
|
||||||
if (_targetPos == _currentPos)
|
|
||||||
return false;
|
|
||||||
if (_targetPos >_currentPos)
|
|
||||||
_direction = DIRECTION_CW;
|
|
||||||
else
|
|
||||||
_direction = DIRECTION_CCW;
|
|
||||||
return runSpeed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocks until the new target position is reached
|
|
||||||
void AccelStepper::runToNewPosition(long position)
|
|
||||||
{
|
|
||||||
moveTo(position);
|
|
||||||
runToPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccelStepper::stop()
|
|
||||||
{
|
|
||||||
if (_speed != 0.0)
|
|
||||||
{
|
|
||||||
long stepsToStop = (long)((_speed * _speed) / (2.0 * _acceleration)) + 1; // Equation 16 (+integer rounding)
|
|
||||||
if (_speed > 0)
|
|
||||||
move(stepsToStop);
|
|
||||||
else
|
|
||||||
move(-stepsToStop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
666
AccelStepper.h
666
AccelStepper.h
@@ -1,666 +0,0 @@
|
|||||||
// AccelStepper.h
|
|
||||||
//
|
|
||||||
/// \mainpage AccelStepper library for Arduino
|
|
||||||
///
|
|
||||||
/// This is the Arduino AccelStepper library.
|
|
||||||
/// It provides an object-oriented interface for 2, 3 or 4 pin stepper motors.
|
|
||||||
///
|
|
||||||
/// The standard Arduino IDE includes the Stepper library
|
|
||||||
/// (http://arduino.cc/en/Reference/Stepper) for stepper motors. It is
|
|
||||||
/// perfectly adequate for simple, single motor applications.
|
|
||||||
///
|
|
||||||
/// AccelStepper significantly improves on the standard Arduino Stepper library in several ways:
|
|
||||||
/// \li Supports acceleration and deceleration
|
|
||||||
/// \li Supports multiple simultaneous steppers, with independent concurrent stepping on each stepper
|
|
||||||
/// \li API functions never delay() or block
|
|
||||||
/// \li Supports 2, 3 and 4 wire steppers, plus 3 and 4 wire half steppers.
|
|
||||||
/// \li Supports alternate stepping functions to enable support of AFMotor (https://github.com/adafruit/Adafruit-Motor-Shield-library)
|
|
||||||
/// \li Supports stepper drivers such as the Sparkfun EasyDriver (based on 3967 driver chip)
|
|
||||||
/// \li Very slow speeds are supported
|
|
||||||
/// \li Extensive API
|
|
||||||
/// \li Subclass support
|
|
||||||
///
|
|
||||||
/// The latest version of this documentation can be downloaded from
|
|
||||||
/// http://www.airspayce.com/mikem/arduino/AccelStepper
|
|
||||||
/// The version of the package that this documentation refers to can be downloaded
|
|
||||||
/// from http://www.airspayce.com/mikem/arduino/AccelStepper/AccelStepper-1.47.zip
|
|
||||||
///
|
|
||||||
/// Example Arduino programs are included to show the main modes of use.
|
|
||||||
///
|
|
||||||
/// You can also find online help and discussion at http://groups.google.com/group/accelstepper
|
|
||||||
/// Please use that group for all questions and discussions on this topic.
|
|
||||||
/// Do not contact the author directly, unless it is to discuss commercial licensing.
|
|
||||||
/// Before asking a question or reporting a bug, please read http://www.catb.org/esr/faqs/smart-questions.html
|
|
||||||
///
|
|
||||||
/// Tested on Arduino Diecimila and Mega with arduino-0018 & arduino-0021
|
|
||||||
/// on OpenSuSE 11.1 and avr-libc-1.6.1-1.15,
|
|
||||||
/// cross-avr-binutils-2.19-9.1, cross-avr-gcc-4.1.3_20080612-26.5.
|
|
||||||
/// Tested on Teensy http://www.pjrc.com/teensy including Teensy 3.1 built using Arduino IDE 1.0.5 with
|
|
||||||
/// teensyduino addon 1.18 and later.
|
|
||||||
///
|
|
||||||
/// \par Installation
|
|
||||||
///
|
|
||||||
/// Install in the usual way: unzip the distribution zip file to the libraries
|
|
||||||
/// sub-folder of your sketchbook.
|
|
||||||
///
|
|
||||||
/// \par Theory
|
|
||||||
///
|
|
||||||
/// This code uses speed calculations as described in
|
|
||||||
/// "Generate stepper-motor speed profiles in real time" by David Austin
|
|
||||||
/// http://fab.cba.mit.edu/classes/MIT/961.09/projects/i0/Stepper_Motor_Speed_Profile.pdf
|
|
||||||
/// with the exception that AccelStepper uses steps per second rather than radians per second
|
|
||||||
/// (because we dont know the step angle of the motor)
|
|
||||||
/// An initial step interval is calculated for the first step, based on the desired acceleration
|
|
||||||
/// On subsequent steps, shorter step intervals are calculated based
|
|
||||||
/// on the previous step until max speed is achieved.
|
|
||||||
///
|
|
||||||
/// \par Donations
|
|
||||||
///
|
|
||||||
/// This library is offered under a free GPL license for those who want to use it that way.
|
|
||||||
/// We try hard to keep it up to date, fix bugs
|
|
||||||
/// and to provide free support. If this library has helped you save time or money, please consider donating at
|
|
||||||
/// http://www.airspayce.com or here:
|
|
||||||
///
|
|
||||||
/// \htmlonly <form action="https://www.paypal.com/cgi-bin/webscr" method="post"><input type="hidden" name="cmd" value="_donations" /> <input type="hidden" name="business" value="mikem@airspayce.com" /> <input type="hidden" name="lc" value="AU" /> <input type="hidden" name="item_name" value="Airspayce" /> <input type="hidden" name="item_number" value="AccelStepper" /> <input type="hidden" name="currency_code" value="USD" /> <input type="hidden" name="bn" value="PP-DonationsBF:btn_donateCC_LG.gif:NonHosted" /> <input type="image" alt="PayPal — The safer, easier way to pay online." name="submit" src="https://www.paypalobjects.com/en_AU/i/btn/btn_donateCC_LG.gif" /> <img alt="" src="https://www.paypalobjects.com/en_AU/i/scr/pixel.gif" width="1" height="1" border="0" /></form> \endhtmlonly
|
|
||||||
///
|
|
||||||
/// \par Trademarks
|
|
||||||
///
|
|
||||||
/// AccelStepper is a trademark of AirSpayce Pty Ltd. The AccelStepper mark was first used on April 26 2010 for
|
|
||||||
/// international trade, and is used only in relation to motor control hardware and software.
|
|
||||||
/// It is not to be confused with any other similar marks covering other goods and services.
|
|
||||||
///
|
|
||||||
/// \par Copyright
|
|
||||||
///
|
|
||||||
/// This software is Copyright (C) 2010 Mike McCauley. Use is subject to license
|
|
||||||
/// conditions. The main licensing options available are GPL V2 or Commercial:
|
|
||||||
///
|
|
||||||
/// \par Open Source Licensing GPL V2
|
|
||||||
/// This is the appropriate option if you want to share the source code of your
|
|
||||||
/// application with everyone you distribute it to, and you also want to give them
|
|
||||||
/// the right to share who uses it. If you wish to use this software under Open
|
|
||||||
/// Source Licensing, you must contribute all your source code to the open source
|
|
||||||
/// community in accordance with the GPL Version 2 when your application is
|
|
||||||
/// distributed. See http://www.gnu.org/copyleft/gpl.html
|
|
||||||
///
|
|
||||||
/// \par Commercial Licensing
|
|
||||||
/// This is the appropriate option if you are creating proprietary applications
|
|
||||||
/// and you are not prepared to distribute and share the source code of your
|
|
||||||
/// application. Contact info@airspayce.com for details.
|
|
||||||
///
|
|
||||||
/// \par Revision History
|
|
||||||
/// \version 1.0 Initial release
|
|
||||||
///
|
|
||||||
/// \version 1.1 Added speed() function to get the current speed.
|
|
||||||
/// \version 1.2 Added runSpeedToPosition() submitted by Gunnar Arndt.
|
|
||||||
/// \version 1.3 Added support for stepper drivers (ie with Step and Direction inputs) with _pins == 1
|
|
||||||
/// \version 1.4 Added functional contructor to support AFMotor, contributed by Limor, with example sketches.
|
|
||||||
/// \version 1.5 Improvements contributed by Peter Mousley: Use of microsecond steps and other speed improvements
|
|
||||||
/// to increase max stepping speed to about 4kHz. New option for user to set the min allowed pulse width.
|
|
||||||
/// Added checks for already running at max speed and skip further calcs if so.
|
|
||||||
/// \version 1.6 Fixed a problem with wrapping of microsecond stepping that could cause stepping to hang.
|
|
||||||
/// Reported by Sandy Noble.
|
|
||||||
/// Removed redundant _lastRunTime member.
|
|
||||||
/// \version 1.7 Fixed a bug where setCurrentPosition() did not always work as expected.
|
|
||||||
/// Reported by Peter Linhart.
|
|
||||||
/// \version 1.8 Added support for 4 pin half-steppers, requested by Harvey Moon
|
|
||||||
/// \version 1.9 setCurrentPosition() now also sets motor speed to 0.
|
|
||||||
/// \version 1.10 Builds on Arduino 1.0
|
|
||||||
/// \version 1.11 Improvments from Michael Ellison:
|
|
||||||
/// Added optional enable line support for stepper drivers
|
|
||||||
/// Added inversion for step/direction/enable lines for stepper drivers
|
|
||||||
/// \version 1.12 Announce Google Group
|
|
||||||
/// \version 1.13 Improvements to speed calculation. Cost of calculation is now less in the worst case,
|
|
||||||
/// and more or less constant in all cases. This should result in slightly beter high speed performance, and
|
|
||||||
/// reduce anomalous speed glitches when other steppers are accelerating.
|
|
||||||
/// However, its hard to see how to replace the sqrt() required at the very first step from 0 speed.
|
|
||||||
/// \version 1.14 Fixed a problem with compiling under arduino 0021 reported by EmbeddedMan
|
|
||||||
/// \version 1.15 Fixed a problem with runSpeedToPosition which did not correctly handle
|
|
||||||
/// running backwards to a smaller target position. Added examples
|
|
||||||
/// \version 1.16 Fixed some cases in the code where abs() was used instead of fabs().
|
|
||||||
/// \version 1.17 Added example ProportionalControl
|
|
||||||
/// \version 1.18 Fixed a problem: If one calls the funcion runSpeed() when Speed is zero, it makes steps
|
|
||||||
/// without counting. reported by Friedrich, Klappenbach.
|
|
||||||
/// \version 1.19 Added MotorInterfaceType and symbolic names for the number of pins to use
|
|
||||||
/// for the motor interface. Updated examples to suit.
|
|
||||||
/// Replaced individual pin assignment variables _pin1, _pin2 etc with array _pin[4].
|
|
||||||
/// _pins member changed to _interface.
|
|
||||||
/// Added _pinInverted array to simplify pin inversion operations.
|
|
||||||
/// Added new function setOutputPins() which sets the motor output pins.
|
|
||||||
/// It can be overridden in order to provide, say, serial output instead of parallel output
|
|
||||||
/// Some refactoring and code size reduction.
|
|
||||||
/// \version 1.20 Improved documentation and examples to show need for correctly
|
|
||||||
/// specifying AccelStepper::FULL4WIRE and friends.
|
|
||||||
/// \version 1.21 Fixed a problem where desiredSpeed could compute the wrong step acceleration
|
|
||||||
/// when _speed was small but non-zero. Reported by Brian Schmalz.
|
|
||||||
/// Precompute sqrt_twoa to improve performance and max possible stepping speed
|
|
||||||
/// \version 1.22 Added Bounce.pde example
|
|
||||||
/// Fixed a problem where calling moveTo(), setMaxSpeed(), setAcceleration() more
|
|
||||||
/// frequently than the step time, even
|
|
||||||
/// with the same values, would interfere with speed calcs. Now a new speed is computed
|
|
||||||
/// only if there was a change in the set value. Reported by Brian Schmalz.
|
|
||||||
/// \version 1.23 Rewrite of the speed algorithms in line with
|
|
||||||
/// http://fab.cba.mit.edu/classes/MIT/961.09/projects/i0/Stepper_Motor_Speed_Profile.pdf
|
|
||||||
/// Now expect smoother and more linear accelerations and decelerations. The desiredSpeed()
|
|
||||||
/// function was removed.
|
|
||||||
/// \version 1.24 Fixed a problem introduced in 1.23: with runToPosition, which did never returned
|
|
||||||
/// \version 1.25 Now ignore attempts to set acceleration to 0.0
|
|
||||||
/// \version 1.26 Fixed a problem where certina combinations of speed and accelration could cause
|
|
||||||
/// oscillation about the target position.
|
|
||||||
/// \version 1.27 Added stop() function to stop as fast as possible with current acceleration parameters.
|
|
||||||
/// Also added new Quickstop example showing its use.
|
|
||||||
/// \version 1.28 Fixed another problem where certain combinations of speed and accelration could cause
|
|
||||||
/// oscillation about the target position.
|
|
||||||
/// Added support for 3 wire full and half steppers such as Hard Disk Drive spindle.
|
|
||||||
/// Contributed by Yuri Ivatchkovitch.
|
|
||||||
/// \version 1.29 Fixed a problem that could cause a DRIVER stepper to continually step
|
|
||||||
/// with some sketches. Reported by Vadim.
|
|
||||||
/// \version 1.30 Fixed a problem that could cause stepper to back up a few steps at the end of
|
|
||||||
/// accelerated travel with certain speeds. Reported and patched by jolo.
|
|
||||||
/// \version 1.31 Updated author and distribution location details to airspayce.com
|
|
||||||
/// \version 1.32 Fixed a problem with enableOutputs() and setEnablePin on Arduino Due that
|
|
||||||
/// prevented the enable pin changing stae correctly. Reported by Duane Bishop.
|
|
||||||
/// \version 1.33 Fixed an error in example AFMotor_ConstantSpeed.pde did not setMaxSpeed();
|
|
||||||
/// Fixed a problem that caused incorrect pin sequencing of FULL3WIRE and HALF3WIRE.
|
|
||||||
/// Unfortunately this meant changing the signature for all step*() functions.
|
|
||||||
/// Added example MotorShield, showing how to use AdaFruit Motor Shield to control
|
|
||||||
/// a 3 phase motor such as a HDD spindle motor (and without using the AFMotor library.
|
|
||||||
/// \version 1.34 Added setPinsInverted(bool pin1Invert, bool pin2Invert, bool pin3Invert, bool pin4Invert, bool enableInvert)
|
|
||||||
/// to allow inversion of 2, 3 and 4 wire stepper pins. Requested by Oleg.
|
|
||||||
/// \version 1.35 Removed default args from setPinsInverted(bool, bool, bool, bool, bool) to prevent ambiguity with
|
|
||||||
/// setPinsInverted(bool, bool, bool). Reported by Mac Mac.
|
|
||||||
/// \version 1.36 Changed enableOutputs() and disableOutputs() to be virtual so can be overridden.
|
|
||||||
/// Added new optional argument 'enable' to constructor, which allows you toi disable the
|
|
||||||
/// automatic enabling of outputs at construction time. Suggested by Guido.
|
|
||||||
/// \version 1.37 Fixed a problem with step1 that could cause a rogue step in the
|
|
||||||
/// wrong direction (or not,
|
|
||||||
/// depending on the setup-time requirements of the connected hardware).
|
|
||||||
/// Reported by Mark Tillotson.
|
|
||||||
/// \version 1.38 run() function incorrectly always returned true. Updated function and doc so it returns true
|
|
||||||
/// if the motor is still running to the target position.
|
|
||||||
/// \version 1.39 Updated typos in keywords.txt, courtesey Jon Magill.
|
|
||||||
/// \version 1.40 Updated documentation, including testing on Teensy 3.1
|
|
||||||
/// \version 1.41 Fixed an error in the acceleration calculations, resulting in acceleration of haldf the intended value
|
|
||||||
/// \version 1.42 Improved support for FULL3WIRE and HALF3WIRE output pins. These changes were in Yuri's original
|
|
||||||
/// contribution but did not make it into production.<br>
|
|
||||||
/// \version 1.43 Added DualMotorShield example. Shows how to use AccelStepper to control 2 x 2 phase steppers using the
|
|
||||||
/// Itead Studio Arduino Dual Stepper Motor Driver Shield model IM120417015.<br>
|
|
||||||
/// \version 1.44 examples/DualMotorShield/DualMotorShield.ino examples/DualMotorShield/DualMotorShield.pde
|
|
||||||
/// was missing from the distribution.<br>
|
|
||||||
/// \version 1.45 Fixed a problem where if setAcceleration was not called, there was no default
|
|
||||||
/// acceleration. Reported by Michael Newman.<br>
|
|
||||||
/// \version 1.45 Fixed inaccuracy in acceleration rate by using Equation 15, suggested by Sebastian Gracki.<br>
|
|
||||||
/// Performance improvements in runSpeed suggested by Jaakko Fagerlund.<br>
|
|
||||||
/// \version 1.46 Fixed error in documentation for runToPosition().
|
|
||||||
/// Reinstated time calculations in runSpeed() since new version is reported
|
|
||||||
/// not to work correctly under some circumstances. Reported by Oleg V Gavva.<br>
|
|
||||||
|
|
||||||
///
|
|
||||||
/// \author Mike McCauley (mikem@airspayce.com) DO NOT CONTACT THE AUTHOR DIRECTLY: USE THE LISTS
|
|
||||||
// Copyright (C) 2009-2013 Mike McCauley
|
|
||||||
// $Id: AccelStepper.h,v 1.21 2014/10/31 06:05:30 mikem Exp mikem $
|
|
||||||
|
|
||||||
#ifndef AccelStepper_h
|
|
||||||
#define AccelStepper_h
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#if ARDUINO >= 100
|
|
||||||
#include <Arduino.h>
|
|
||||||
#else
|
|
||||||
#include <WProgram.h>
|
|
||||||
#include <wiring.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// These defs cause trouble on some versions of Arduino
|
|
||||||
#undef round
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
|
||||||
/// \class AccelStepper AccelStepper.h <AccelStepper.h>
|
|
||||||
/// \brief Support for stepper motors with acceleration etc.
|
|
||||||
///
|
|
||||||
/// This defines a single 2 or 4 pin stepper motor, or stepper moter with fdriver chip, with optional
|
|
||||||
/// acceleration, deceleration, absolute positioning commands etc. Multiple
|
|
||||||
/// simultaneous steppers are supported, all moving
|
|
||||||
/// at different speeds and accelerations.
|
|
||||||
///
|
|
||||||
/// \par Operation
|
|
||||||
/// This module operates by computing a step time in microseconds. The step
|
|
||||||
/// time is recomputed after each step and after speed and acceleration
|
|
||||||
/// parameters are changed by the caller. The time of each step is recorded in
|
|
||||||
/// microseconds. The run() function steps the motor once if a new step is due.
|
|
||||||
/// The run() function must be called frequently until the motor is in the
|
|
||||||
/// desired position, after which time run() will do nothing.
|
|
||||||
///
|
|
||||||
/// \par Positioning
|
|
||||||
/// Positions are specified by a signed long integer. At
|
|
||||||
/// construction time, the current position of the motor is consider to be 0. Positive
|
|
||||||
/// positions are clockwise from the initial position; negative positions are
|
|
||||||
/// anticlockwise. The current position can be altered for instance after
|
|
||||||
/// initialization positioning.
|
|
||||||
///
|
|
||||||
/// \par Caveats
|
|
||||||
/// This is an open loop controller: If the motor stalls or is oversped,
|
|
||||||
/// AccelStepper will not have a correct
|
|
||||||
/// idea of where the motor really is (since there is no feedback of the motor's
|
|
||||||
/// real position. We only know where we _think_ it is, relative to the
|
|
||||||
/// initial starting point).
|
|
||||||
///
|
|
||||||
/// \par Performance
|
|
||||||
/// The fastest motor speed that can be reliably supported is about 4000 steps per
|
|
||||||
/// second at a clock frequency of 16 MHz on Arduino such as Uno etc.
|
|
||||||
/// Faster processors can support faster stepping speeds.
|
|
||||||
/// However, any speed less than that
|
|
||||||
/// down to very slow speeds (much less than one per second) are also supported,
|
|
||||||
/// provided the run() function is called frequently enough to step the motor
|
|
||||||
/// whenever required for the speed set.
|
|
||||||
/// Calling setAcceleration() is expensive,
|
|
||||||
/// since it requires a square root to be calculated.
|
|
||||||
class AccelStepper
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
/// \brief Symbolic names for number of pins.
|
|
||||||
/// Use this in the pins argument the AccelStepper constructor to
|
|
||||||
/// provide a symbolic name for the number of pins
|
|
||||||
/// to use.
|
|
||||||
typedef enum
|
|
||||||
{
|
|
||||||
FUNCTION = 0, ///< Use the functional interface, implementing your own driver functions (internal use only)
|
|
||||||
DRIVER = 1, ///< Stepper Driver, 2 driver pins required
|
|
||||||
FULL2WIRE = 2, ///< 2 wire stepper, 2 motor pins required
|
|
||||||
FULL3WIRE = 3, ///< 3 wire stepper, such as HDD spindle, 3 motor pins required
|
|
||||||
FULL4WIRE = 4, ///< 4 wire full stepper, 4 motor pins required
|
|
||||||
HALF3WIRE = 6, ///< 3 wire half stepper, such as HDD spindle, 3 motor pins required
|
|
||||||
HALF4WIRE = 8 ///< 4 wire half stepper, 4 motor pins required
|
|
||||||
} MotorInterfaceType;
|
|
||||||
|
|
||||||
/// Constructor. You can have multiple simultaneous steppers, all moving
|
|
||||||
/// at different speeds and accelerations, provided you call their run()
|
|
||||||
/// functions at frequent enough intervals. Current Position is set to 0, target
|
|
||||||
/// position is set to 0. MaxSpeed and Acceleration default to 1.0.
|
|
||||||
/// The motor pins will be initialised to OUTPUT mode during the
|
|
||||||
/// constructor by a call to enableOutputs().
|
|
||||||
/// \param[in] interface Number of pins to interface to. 1, 2, 4 or 8 are
|
|
||||||
/// supported, but it is preferred to use the \ref MotorInterfaceType symbolic names.
|
|
||||||
/// AccelStepper::DRIVER (1) means a stepper driver (with Step and Direction pins).
|
|
||||||
/// If an enable line is also needed, call setEnablePin() after construction.
|
|
||||||
/// You may also invert the pins using setPinsInverted().
|
|
||||||
/// AccelStepper::FULL2WIRE (2) means a 2 wire stepper (2 pins required).
|
|
||||||
/// AccelStepper::FULL3WIRE (3) means a 3 wire stepper, such as HDD spindle (3 pins required).
|
|
||||||
/// AccelStepper::FULL4WIRE (4) means a 4 wire stepper (4 pins required).
|
|
||||||
/// AccelStepper::HALF3WIRE (6) means a 3 wire half stepper, such as HDD spindle (3 pins required)
|
|
||||||
/// AccelStepper::HALF4WIRE (8) means a 4 wire half stepper (4 pins required)
|
|
||||||
/// Defaults to AccelStepper::FULL4WIRE (4) pins.
|
|
||||||
/// \param[in] pin1 Arduino digital pin number for motor pin 1. Defaults
|
|
||||||
/// to pin 2. For a AccelStepper::DRIVER (interface==1),
|
|
||||||
/// this is the Step input to the driver. Low to high transition means to step)
|
|
||||||
/// \param[in] pin2 Arduino digital pin number for motor pin 2. Defaults
|
|
||||||
/// to pin 3. For a AccelStepper::DRIVER (interface==1),
|
|
||||||
/// this is the Direction input the driver. High means forward.
|
|
||||||
/// \param[in] pin3 Arduino digital pin number for motor pin 3. Defaults
|
|
||||||
/// to pin 4.
|
|
||||||
/// \param[in] pin4 Arduino digital pin number for motor pin 4. Defaults
|
|
||||||
/// to pin 5.
|
|
||||||
/// \param[in] enable If this is true (the default), enableOutputs() will be called to enable
|
|
||||||
/// the output pins at construction time.
|
|
||||||
AccelStepper(uint8_t interface = AccelStepper::FULL4WIRE, uint8_t pin1 = 2, uint8_t pin2 = 3, uint8_t pin3 = 4, uint8_t pin4 = 5, bool enable = true);
|
|
||||||
|
|
||||||
/// Alternate Constructor which will call your own functions for forward and backward steps.
|
|
||||||
/// You can have multiple simultaneous steppers, all moving
|
|
||||||
/// at different speeds and accelerations, provided you call their run()
|
|
||||||
/// functions at frequent enough intervals. Current Position is set to 0, target
|
|
||||||
/// position is set to 0. MaxSpeed and Acceleration default to 1.0.
|
|
||||||
/// Any motor initialization should happen before hand, no pins are used or initialized.
|
|
||||||
/// \param[in] forward void-returning procedure that will make a forward step
|
|
||||||
/// \param[in] backward void-returning procedure that will make a backward step
|
|
||||||
AccelStepper(void (*forward)(), void (*backward)());
|
|
||||||
|
|
||||||
/// Set the target position. The run() function will try to move the motor (at most one step per call)
|
|
||||||
/// from the current position to the target position set by the most
|
|
||||||
/// recent call to this function. Caution: moveTo() also recalculates the speed for the next step.
|
|
||||||
/// If you are trying to use constant speed movements, you should call setSpeed() after calling moveTo().
|
|
||||||
/// \param[in] absolute The desired absolute position. Negative is
|
|
||||||
/// anticlockwise from the 0 position.
|
|
||||||
void moveTo(long absolute);
|
|
||||||
|
|
||||||
/// Set the target position relative to the current position
|
|
||||||
/// \param[in] relative The desired position relative to the current position. Negative is
|
|
||||||
/// anticlockwise from the current position.
|
|
||||||
void move(long relative);
|
|
||||||
|
|
||||||
/// Poll the motor and step it if a step is due, implementing
|
|
||||||
/// accelerations and decelerations to acheive the target position. You must call this as
|
|
||||||
/// frequently as possible, but at least once per minimum step time interval,
|
|
||||||
/// preferably in your main loop. Note that each call to run() will make at most one step, and then only when a step is due,
|
|
||||||
/// based on the current speed and the time since the last step.
|
|
||||||
/// \return true if the motor is still running to the target position.
|
|
||||||
boolean run();
|
|
||||||
|
|
||||||
/// Poll the motor and step it if a step is due, implementing a constant
|
|
||||||
/// speed as set by the most recent call to setSpeed(). You must call this as
|
|
||||||
/// frequently as possible, but at least once per step interval,
|
|
||||||
/// \return true if the motor was stepped.
|
|
||||||
boolean runSpeed();
|
|
||||||
|
|
||||||
/// Sets the maximum permitted speed. The run() function will accelerate
|
|
||||||
/// up to the speed set by this function.
|
|
||||||
/// Caution: the maximum speed achievable depends on your processor and clock speed.
|
|
||||||
/// \param[in] speed The desired maximum speed in steps per second. Must
|
|
||||||
/// be > 0. Caution: Speeds that exceed the maximum speed supported by the processor may
|
|
||||||
/// Result in non-linear accelerations and decelerations.
|
|
||||||
void setMaxSpeed(float speed);
|
|
||||||
|
|
||||||
/// Sets the acceleration/deceleration rate.
|
|
||||||
/// \param[in] acceleration The desired acceleration in steps per second
|
|
||||||
/// per second. Must be > 0.0. This is an expensive call since it requires a square
|
|
||||||
/// root to be calculated. Dont call more ofthen than needed
|
|
||||||
void setAcceleration(float acceleration);
|
|
||||||
|
|
||||||
/// Sets the desired constant speed for use with runSpeed().
|
|
||||||
/// \param[in] speed The desired constant speed in steps per
|
|
||||||
/// second. Positive is clockwise. Speeds of more than 1000 steps per
|
|
||||||
/// second are unreliable. Very slow speeds may be set (eg 0.00027777 for
|
|
||||||
/// once per hour, approximately. Speed accuracy depends on the Arduino
|
|
||||||
/// crystal. Jitter depends on how frequently you call the runSpeed() function.
|
|
||||||
void setSpeed(float speed);
|
|
||||||
|
|
||||||
/// The most recently set speed
|
|
||||||
/// \return the most recent speed in steps per second
|
|
||||||
float speed();
|
|
||||||
|
|
||||||
/// The distance from the current position to the target position.
|
|
||||||
/// \return the distance from the current position to the target position
|
|
||||||
/// in steps. Positive is clockwise from the current position.
|
|
||||||
long distanceToGo();
|
|
||||||
|
|
||||||
/// The most recently set target position.
|
|
||||||
/// \return the target position
|
|
||||||
/// in steps. Positive is clockwise from the 0 position.
|
|
||||||
long targetPosition();
|
|
||||||
|
|
||||||
/// The currently motor position.
|
|
||||||
/// \return the current motor position
|
|
||||||
/// in steps. Positive is clockwise from the 0 position.
|
|
||||||
long currentPosition();
|
|
||||||
|
|
||||||
/// Resets the current position of the motor, so that wherever the motor
|
|
||||||
/// happens to be right now is considered to be the new 0 position. Useful
|
|
||||||
/// for setting a zero position on a stepper after an initial hardware
|
|
||||||
/// positioning move.
|
|
||||||
/// Has the side effect of setting the current motor speed to 0.
|
|
||||||
/// \param[in] position The position in steps of wherever the motor
|
|
||||||
/// happens to be right now.
|
|
||||||
void setCurrentPosition(long position);
|
|
||||||
|
|
||||||
/// Moves the motor (with acceleration/deceleration)
|
|
||||||
/// to the target position and blocks until it is at
|
|
||||||
/// position. Dont use this in event loops, since it blocks.
|
|
||||||
void runToPosition();
|
|
||||||
|
|
||||||
/// Runs at the currently selected speed until the target position is reached
|
|
||||||
/// Does not implement accelerations.
|
|
||||||
/// \return true if it stepped
|
|
||||||
boolean runSpeedToPosition();
|
|
||||||
|
|
||||||
/// Moves the motor (with acceleration/deceleration)
|
|
||||||
/// to the new target position and blocks until it is at
|
|
||||||
/// position. Dont use this in event loops, since it blocks.
|
|
||||||
/// \param[in] position The new target position.
|
|
||||||
void runToNewPosition(long position);
|
|
||||||
|
|
||||||
/// Sets a new target position that causes the stepper
|
|
||||||
/// to stop as quickly as possible, using the current speed and acceleration parameters.
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
/// Disable motor pin outputs by setting them all LOW
|
|
||||||
/// Depending on the design of your electronics this may turn off
|
|
||||||
/// the power to the motor coils, saving power.
|
|
||||||
/// This is useful to support Arduino low power modes: disable the outputs
|
|
||||||
/// during sleep and then reenable with enableOutputs() before stepping
|
|
||||||
/// again.
|
|
||||||
virtual void disableOutputs();
|
|
||||||
|
|
||||||
/// Enable motor pin outputs by setting the motor pins to OUTPUT
|
|
||||||
/// mode. Called automatically by the constructor.
|
|
||||||
virtual void enableOutputs();
|
|
||||||
|
|
||||||
/// Sets the minimum pulse width allowed by the stepper driver. The minimum practical pulse width is
|
|
||||||
/// approximately 20 microseconds. Times less than 20 microseconds
|
|
||||||
/// will usually result in 20 microseconds or so.
|
|
||||||
/// \param[in] minWidth The minimum pulse width in microseconds.
|
|
||||||
void setMinPulseWidth(unsigned int minWidth);
|
|
||||||
|
|
||||||
/// Sets the enable pin number for stepper drivers.
|
|
||||||
/// 0xFF indicates unused (default).
|
|
||||||
/// Otherwise, if a pin is set, the pin will be turned on when
|
|
||||||
/// enableOutputs() is called and switched off when disableOutputs()
|
|
||||||
/// is called.
|
|
||||||
/// \param[in] enablePin Arduino digital pin number for motor enable
|
|
||||||
/// \sa setPinsInverted
|
|
||||||
void setEnablePin(uint8_t enablePin = 0xff);
|
|
||||||
|
|
||||||
/// Sets the inversion for stepper driver pins
|
|
||||||
/// \param[in] directionInvert True for inverted direction pin, false for non-inverted
|
|
||||||
/// \param[in] stepInvert True for inverted step pin, false for non-inverted
|
|
||||||
/// \param[in] enableInvert True for inverted enable pin, false (default) for non-inverted
|
|
||||||
void setPinsInverted(bool directionInvert = false, bool stepInvert = false, bool enableInvert = false);
|
|
||||||
|
|
||||||
/// Sets the inversion for 2, 3 and 4 wire stepper pins
|
|
||||||
/// \param[in] pin1Invert True for inverted pin1, false for non-inverted
|
|
||||||
/// \param[in] pin2Invert True for inverted pin2, false for non-inverted
|
|
||||||
/// \param[in] pin3Invert True for inverted pin3, false for non-inverted
|
|
||||||
/// \param[in] pin4Invert True for inverted pin4, false for non-inverted
|
|
||||||
/// \param[in] enableInvert True for inverted enable pin, false (default) for non-inverted
|
|
||||||
void setPinsInverted(bool pin1Invert, bool pin2Invert, bool pin3Invert, bool pin4Invert, bool enableInvert);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
|
|
||||||
/// \brief Direction indicator
|
|
||||||
/// Symbolic names for the direction the motor is turning
|
|
||||||
typedef enum
|
|
||||||
{
|
|
||||||
DIRECTION_CCW = 0, ///< Clockwise
|
|
||||||
DIRECTION_CW = 1 ///< Counter-Clockwise
|
|
||||||
} Direction;
|
|
||||||
|
|
||||||
/// Forces the library to compute a new instantaneous speed and set that as
|
|
||||||
/// the current speed. It is called by
|
|
||||||
/// the library:
|
|
||||||
/// \li after each step
|
|
||||||
/// \li after change to maxSpeed through setMaxSpeed()
|
|
||||||
/// \li after change to acceleration through setAcceleration()
|
|
||||||
/// \li after change to target position (relative or absolute) through
|
|
||||||
/// move() or moveTo()
|
|
||||||
void computeNewSpeed();
|
|
||||||
|
|
||||||
/// Low level function to set the motor output pins
|
|
||||||
/// bit 0 of the mask corresponds to _pin[0]
|
|
||||||
/// bit 1 of the mask corresponds to _pin[1]
|
|
||||||
/// You can override this to impment, for example serial chip output insted of using the
|
|
||||||
/// output pins directly
|
|
||||||
virtual void setOutputPins(uint8_t mask);
|
|
||||||
|
|
||||||
/// Called to execute a step. Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default calls step1(), step2(), step4() or step8() depending on the
|
|
||||||
/// number of pins defined for the stepper.
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step using stepper functions (pins = 0) Only called when a new step is
|
|
||||||
/// required. Calls _forward() or _backward() to perform the step
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step0(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step on a stepper driver (ie where pins == 1). Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default sets or clears the outputs of Step pin1 to step,
|
|
||||||
/// and sets the output of _pin2 to the desired direction. The Step pin (_pin1) is pulsed for 1 microsecond
|
|
||||||
/// which is the minimum STEP pulse width for the 3967 driver.
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step1(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step on a 2 pin motor. Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default sets or clears the outputs of pin1 and pin2
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step2(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step on a 3 pin motor, such as HDD spindle. Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default sets or clears the outputs of pin1, pin2,
|
|
||||||
/// pin3
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step3(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step on a 4 pin motor. Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default sets or clears the outputs of pin1, pin2,
|
|
||||||
/// pin3, pin4.
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step4(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step on a 3 pin motor, such as HDD spindle. Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default sets or clears the outputs of pin1, pin2,
|
|
||||||
/// pin3
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step6(long step);
|
|
||||||
|
|
||||||
/// Called to execute a step on a 4 pin half-steper motor. Only called when a new step is
|
|
||||||
/// required. Subclasses may override to implement new stepping
|
|
||||||
/// interfaces. The default sets or clears the outputs of pin1, pin2,
|
|
||||||
/// pin3, pin4.
|
|
||||||
/// \param[in] step The current step phase number (0 to 7)
|
|
||||||
virtual void step8(long step);
|
|
||||||
|
|
||||||
private:
|
|
||||||
/// Number of pins on the stepper motor. Permits 2 or 4. 2 pins is a
|
|
||||||
/// bipolar, and 4 pins is a unipolar.
|
|
||||||
uint8_t _interface; // 0, 1, 2, 4, 8, See MotorInterfaceType
|
|
||||||
|
|
||||||
/// Arduino pin number assignments for the 2 or 4 pins required to interface to the
|
|
||||||
/// stepper motor or driver
|
|
||||||
uint8_t _pin[4];
|
|
||||||
|
|
||||||
/// Whether the _pins is inverted or not
|
|
||||||
uint8_t _pinInverted[4];
|
|
||||||
|
|
||||||
/// The current absolution position in steps.
|
|
||||||
long _currentPos; // Steps
|
|
||||||
|
|
||||||
/// The target position in steps. The AccelStepper library will move the
|
|
||||||
/// motor from the _currentPos to the _targetPos, taking into account the
|
|
||||||
/// max speed, acceleration and deceleration
|
|
||||||
long _targetPos; // Steps
|
|
||||||
|
|
||||||
/// The current motos speed in steps per second
|
|
||||||
/// Positive is clockwise
|
|
||||||
float _speed; // Steps per second
|
|
||||||
|
|
||||||
/// The maximum permitted speed in steps per second. Must be > 0.
|
|
||||||
float _maxSpeed;
|
|
||||||
|
|
||||||
/// The acceleration to use to accelerate or decelerate the motor in steps
|
|
||||||
/// per second per second. Must be > 0
|
|
||||||
float _acceleration;
|
|
||||||
float _sqrt_twoa; // Precomputed sqrt(2*_acceleration)
|
|
||||||
|
|
||||||
/// The current interval between steps in microseconds.
|
|
||||||
/// 0 means the motor is currently stopped with _speed == 0
|
|
||||||
unsigned long _stepInterval;
|
|
||||||
|
|
||||||
/// The last step time in microseconds
|
|
||||||
unsigned long _lastStepTime;
|
|
||||||
|
|
||||||
/// The minimum allowed pulse width in microseconds
|
|
||||||
unsigned int _minPulseWidth;
|
|
||||||
|
|
||||||
/// Is the direction pin inverted?
|
|
||||||
///bool _dirInverted; /// Moved to _pinInverted[1]
|
|
||||||
|
|
||||||
/// Is the step pin inverted?
|
|
||||||
///bool _stepInverted; /// Moved to _pinInverted[0]
|
|
||||||
|
|
||||||
/// Is the enable pin inverted?
|
|
||||||
bool _enableInverted;
|
|
||||||
|
|
||||||
/// Enable pin for stepper driver, or 0xFF if unused.
|
|
||||||
uint8_t _enablePin;
|
|
||||||
|
|
||||||
/// The pointer to a forward-step procedure
|
|
||||||
void (*_forward)();
|
|
||||||
|
|
||||||
/// The pointer to a backward-step procedure
|
|
||||||
void (*_backward)();
|
|
||||||
|
|
||||||
/// The step counter for speed calculations
|
|
||||||
long _n;
|
|
||||||
|
|
||||||
/// Initial step size in microseconds
|
|
||||||
float _c0;
|
|
||||||
|
|
||||||
/// Last step size in microseconds
|
|
||||||
float _cn;
|
|
||||||
|
|
||||||
/// Min step size in microseconds based on maxSpeed
|
|
||||||
float _cmin; // at max speed
|
|
||||||
|
|
||||||
/// Current direction motor is spinning in
|
|
||||||
boolean _direction; // 1 == CW
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/// @example Random.pde
|
|
||||||
/// Make a single stepper perform random changes in speed, position and acceleration
|
|
||||||
|
|
||||||
/// @example Overshoot.pde
|
|
||||||
/// Check overshoot handling
|
|
||||||
/// which sets a new target position and then waits until the stepper has
|
|
||||||
/// achieved it. This is used for testing the handling of overshoots
|
|
||||||
|
|
||||||
/// @example MultiStepper.pde
|
|
||||||
/// Shows how to multiple simultaneous steppers
|
|
||||||
/// Runs one stepper forwards and backwards, accelerating and decelerating
|
|
||||||
/// at the limits. Runs other steppers at the same time
|
|
||||||
|
|
||||||
/// @example ConstantSpeed.pde
|
|
||||||
/// Shows how to run AccelStepper in the simplest,
|
|
||||||
/// fixed speed mode with no accelerations
|
|
||||||
|
|
||||||
/// @example Blocking.pde
|
|
||||||
/// Shows how to use the blocking call runToNewPosition
|
|
||||||
/// Which sets a new target position and then waits until the stepper has
|
|
||||||
/// achieved it.
|
|
||||||
|
|
||||||
/// @example AFMotor_MultiStepper.pde
|
|
||||||
/// Control both Stepper motors at the same time with different speeds
|
|
||||||
/// and accelerations.
|
|
||||||
|
|
||||||
/// @example AFMotor_ConstantSpeed.pde
|
|
||||||
/// Shows how to run AccelStepper in the simplest,
|
|
||||||
/// fixed speed mode with no accelerations
|
|
||||||
|
|
||||||
/// @example ProportionalControl.pde
|
|
||||||
/// Make a single stepper follow the analog value read from a pot or whatever
|
|
||||||
/// The stepper will move at a constant speed to each newly set posiiton,
|
|
||||||
/// depending on the value of the pot.
|
|
||||||
|
|
||||||
/// @example Bounce.pde
|
|
||||||
/// Make a single stepper bounce from one limit to another, observing
|
|
||||||
/// accelrations at each end of travel
|
|
||||||
|
|
||||||
/// @example Quickstop.pde
|
|
||||||
/// Check stop handling.
|
|
||||||
/// Calls stop() while the stepper is travelling at full speed, causing
|
|
||||||
/// the stepper to stop as quickly as possible, within the constraints of the
|
|
||||||
/// current acceleration.
|
|
||||||
|
|
||||||
/// @example MotorShield.pde
|
|
||||||
/// Shows how to use AccelStepper to control a 3-phase motor, such as a HDD spindle motor
|
|
||||||
/// using the Adafruit Motor Shield http://www.ladyada.net/make/mshield/index.html.
|
|
||||||
|
|
||||||
/// @example DualMotorShield.pde
|
|
||||||
/// Shows how to use AccelStepper to control 2 x 2 phase steppers using the
|
|
||||||
/// Itead Studio Arduino Dual Stepper Motor Driver Shield
|
|
||||||
/// model IM120417015
|
|
||||||
|
|
||||||
#endif
|
|
||||||
109
EggDuino.ino
109
EggDuino.ino
@@ -1,109 +0,0 @@
|
|||||||
/* Eggduino-Firmware by Joachim Cerny, 2014
|
|
||||||
|
|
||||||
Thanks for the nice libs ACCELSTEPPER and SERIALCOMMAND, which made this project much easier.
|
|
||||||
Thanks to the Eggbot-Team for such a funny and enjoable concept!
|
|
||||||
Thanks to my wife and my daughter for their patience. :-)
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// implemented Eggbot-Protocol-Version v13
|
|
||||||
// EBB-Command-Reference, I sourced from: http://www.schmalzhaus.com/EBB/EBBCommands.html
|
|
||||||
// no homing sequence, switch-on position of pen will be taken as reference point.
|
|
||||||
// No collision-detection!!
|
|
||||||
// Supported Servos: I do not know, I use Arduino Servo Lib with TG9e- standard servo.
|
|
||||||
// Note: Maximum-Speed in Inkscape is 1000 Steps/s. You could enter more, but then Pythonscript sends nonsense.
|
|
||||||
// EBB-Coordinates are coming in for 16th-Microstepmode. The Coordinate-Transforms are done in weired integer-math. Be careful, when you diecide to modify settings.
|
|
||||||
|
|
||||||
/* TODOs:
|
|
||||||
1 collision control via penMin/penMax
|
|
||||||
2 implement homing sequence via microswitch or optical device
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "AccelStepper.h" // nice lib from http://www.airspayce.com/mikem/arduino/AccelStepper/
|
|
||||||
#include <Servo.h>
|
|
||||||
#include "SerialCommand.h" //nice lib from Stefan Rado, https://github.com/kroimon/Arduino-SerialCommand
|
|
||||||
#include <avr/eeprom.h>
|
|
||||||
#include "button.h"
|
|
||||||
|
|
||||||
#define initSting "EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a"
|
|
||||||
//Rotational Stepper:
|
|
||||||
#define step1 11
|
|
||||||
#define dir1 10
|
|
||||||
#define enableRotMotor 9
|
|
||||||
#define rotMicrostep 16 //MicrostepMode, only 1,2,4,8,16 allowed, because of Integer-Math in this Sketch
|
|
||||||
//Pen Stepper:
|
|
||||||
#define step2 8
|
|
||||||
#define dir2 7
|
|
||||||
#define enablePenMotor 6
|
|
||||||
#define penMicrostep 16 //MicrostepMode, only 1,2,4,8,16 allowed, because of Integer-Math in this Sketch
|
|
||||||
|
|
||||||
#define servoPin 3 //Servo
|
|
||||||
|
|
||||||
// EXTRAFEATURES - UNCOMMENT TO USE THEM -------------------------------------------------------------------
|
|
||||||
|
|
||||||
// #define prgButton 2 // PRG button
|
|
||||||
// #define penToggleButton 12 // pen up/down button
|
|
||||||
// #define motorsButton 4 // motors enable button
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#define penUpPosEEAddress ((uint16_t *)0)
|
|
||||||
#define penDownPosEEAddress ((uint16_t *)2)
|
|
||||||
|
|
||||||
//make Objects
|
|
||||||
AccelStepper rotMotor(1, step1, dir1);
|
|
||||||
AccelStepper penMotor(1, step2, dir2);
|
|
||||||
Servo penServo;
|
|
||||||
SerialCommand SCmd;
|
|
||||||
//create Buttons
|
|
||||||
#ifdef prgButton
|
|
||||||
Button prgButtonToggle(prgButton, setprgButtonState);
|
|
||||||
#endif
|
|
||||||
#ifdef penToggleButton
|
|
||||||
Button penToggle(penToggleButton, doTogglePen);
|
|
||||||
#endif
|
|
||||||
#ifdef motorsButton
|
|
||||||
Button motorsToggle(motorsButton, toggleMotors);
|
|
||||||
#endif
|
|
||||||
// Variables... be careful, by messing around here, everything has a reason and crossrelations...
|
|
||||||
int penMin=0;
|
|
||||||
int penMax=0;
|
|
||||||
int penUpPos=5; //can be overwritten from EBB-Command SC
|
|
||||||
int penDownPos=20; //can be overwritten from EBB-Command SC
|
|
||||||
int servoRateUp=0; //from EBB-Protocol not implemented on machine-side
|
|
||||||
int servoRateDown=0; //from EBB-Protocol not implemented on machine-side
|
|
||||||
long rotStepError=0;
|
|
||||||
long penStepError=0;
|
|
||||||
int penState=penUpPos;
|
|
||||||
uint32_t nodeCount=0;
|
|
||||||
unsigned int layer=0;
|
|
||||||
boolean prgButtonState=0;
|
|
||||||
uint8_t rotStepCorrection = 16/rotMicrostep ; //devide EBB-Coordinates by this factor to get EGGduino-Steps
|
|
||||||
uint8_t penStepCorrection = 16/penMicrostep ; //devide EBB-Coordinates by this factor to get EGGduino-Steps
|
|
||||||
float rotSpeed=0;
|
|
||||||
float penSpeed=0; // these are local variables for Function SteppermotorMove-Command, but for performance-reasons it will be initialized here
|
|
||||||
boolean motorsEnabled = 0;
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(9600);
|
|
||||||
makeComInterface();
|
|
||||||
initHardware();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
moveOneStep();
|
|
||||||
|
|
||||||
SCmd.readSerial();
|
|
||||||
|
|
||||||
#ifdef penToggleButton
|
|
||||||
penToggle.check();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef motorsButton
|
|
||||||
motorsToggle.check();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef prgButton
|
|
||||||
prgButtonToggle.check();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
264
Functions.ino
264
Functions.ino
@@ -1,264 +0,0 @@
|
|||||||
|
|
||||||
void makeComInterface(){
|
|
||||||
SCmd.addCommand("v",sendVersion);
|
|
||||||
SCmd.addCommand("EM",enableMotors);
|
|
||||||
SCmd.addCommand("SC",stepperModeConfigure);
|
|
||||||
SCmd.addCommand("SP",setPen);
|
|
||||||
SCmd.addCommand("SM",stepperMove);
|
|
||||||
SCmd.addCommand("SE",ignore);
|
|
||||||
SCmd.addCommand("TP",togglePen);
|
|
||||||
SCmd.addCommand("PO",ignore); //Engraver command, not implemented, gives fake answer
|
|
||||||
SCmd.addCommand("NI",nodeCountIncrement);
|
|
||||||
SCmd.addCommand("ND",nodeCountDecrement);
|
|
||||||
SCmd.addCommand("SN",setNodeCount);
|
|
||||||
SCmd.addCommand("QN",queryNodeCount);
|
|
||||||
SCmd.addCommand("SL",setLayer);
|
|
||||||
SCmd.addCommand("QL",queryLayer);
|
|
||||||
SCmd.addCommand("QP",queryPen);
|
|
||||||
SCmd.addCommand("QB",queryButton); //"PRG" Button,
|
|
||||||
SCmd.setDefaultHandler(unrecognized); // Handler for command that isn't matched (says "What?")
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryPen() {
|
|
||||||
char state;
|
|
||||||
if (penState==penUpPos)
|
|
||||||
state='1';
|
|
||||||
else
|
|
||||||
state='0';
|
|
||||||
Serial.print(String(state)+"\r\n");
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryButton() {
|
|
||||||
Serial.print(String(prgButtonState) +"\r\n");
|
|
||||||
sendAck();
|
|
||||||
prgButtonState = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryLayer() {
|
|
||||||
Serial.print(String(layer) +"\r\n");
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setLayer() {
|
|
||||||
uint32_t value=0;
|
|
||||||
char *arg1;
|
|
||||||
arg1 = SCmd.next();
|
|
||||||
if (arg1 != NULL) {
|
|
||||||
value = atoi(arg1);
|
|
||||||
layer=value;
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryNodeCount() {
|
|
||||||
Serial.print(String(nodeCount) +"\r\n");
|
|
||||||
sendAck();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void setNodeCount() {
|
|
||||||
uint32_t value=0;
|
|
||||||
char *arg1;
|
|
||||||
arg1 = SCmd.next();
|
|
||||||
if (arg1 != NULL) {
|
|
||||||
value = atoi(arg1);
|
|
||||||
nodeCount=value;
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void nodeCountIncrement() {
|
|
||||||
nodeCount=nodeCount++;
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
|
|
||||||
void nodeCountDecrement() {
|
|
||||||
nodeCount=nodeCount--;
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
|
|
||||||
void stepperMove() {
|
|
||||||
uint16_t duration=0; //in ms
|
|
||||||
int penStepsEBB=0; //Pen
|
|
||||||
int rotStepsEBB=0; //Rot
|
|
||||||
|
|
||||||
moveToDestination();
|
|
||||||
|
|
||||||
if (!parseSMArgs(&duration, &penStepsEBB, &rotStepsEBB)) {
|
|
||||||
sendError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAck();
|
|
||||||
|
|
||||||
if ( (penStepsEBB==0) && (rotStepsEBB==0) ) {
|
|
||||||
delay(duration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareMove(duration, penStepsEBB, rotStepsEBB);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPen(){
|
|
||||||
int cmd;
|
|
||||||
int value;
|
|
||||||
char *arg;
|
|
||||||
|
|
||||||
moveToDestination();
|
|
||||||
|
|
||||||
arg = SCmd.next();
|
|
||||||
if (arg != NULL) {
|
|
||||||
cmd = atoi(arg);
|
|
||||||
switch (cmd) {
|
|
||||||
case 0:
|
|
||||||
penServo.write(penUpPos);
|
|
||||||
penState=penUpPos;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
penServo.write(penDownPos);
|
|
||||||
penState=penDownPos;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
char *val;
|
|
||||||
val = SCmd.next();
|
|
||||||
if (val != NULL) {
|
|
||||||
value = atoi(val);
|
|
||||||
sendAck();
|
|
||||||
delay(value);
|
|
||||||
}
|
|
||||||
if (val==NULL && arg !=NULL) {
|
|
||||||
sendAck();
|
|
||||||
delay(500);
|
|
||||||
}
|
|
||||||
// Serial.println("delay");
|
|
||||||
if (val==NULL && arg ==NULL)
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void togglePen(){
|
|
||||||
int value;
|
|
||||||
char *arg;
|
|
||||||
|
|
||||||
moveToDestination();
|
|
||||||
|
|
||||||
arg = SCmd.next();
|
|
||||||
if (arg != NULL)
|
|
||||||
value = atoi(arg);
|
|
||||||
else
|
|
||||||
value = 500;
|
|
||||||
|
|
||||||
doTogglePen();
|
|
||||||
sendAck();
|
|
||||||
delay(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void doTogglePen() {
|
|
||||||
if (penState==penUpPos) {
|
|
||||||
penServo.write(penDownPos);
|
|
||||||
penState=penDownPos;
|
|
||||||
} else {
|
|
||||||
penServo.write(penUpPos);
|
|
||||||
penState=penUpPos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void enableMotors(){
|
|
||||||
int cmd;
|
|
||||||
int value;
|
|
||||||
char *arg;
|
|
||||||
char *val;
|
|
||||||
arg = SCmd.next();
|
|
||||||
if (arg != NULL)
|
|
||||||
cmd = atoi(arg);
|
|
||||||
val = SCmd.next();
|
|
||||||
if (val != NULL)
|
|
||||||
value = atoi(val);
|
|
||||||
//values parsed
|
|
||||||
if ((arg != NULL) && (val == NULL)){
|
|
||||||
switch (cmd) {
|
|
||||||
case 0: motorsOff();
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 1: motorsOn();
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//the following implementaion is a little bit cheated, because i did not know, how to implement different values for first and second argument.
|
|
||||||
if ((arg != NULL) && (val != NULL)){
|
|
||||||
switch (value) {
|
|
||||||
case 0: motorsOff();
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 1: motorsOn();
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void stepperModeConfigure(){
|
|
||||||
int cmd;
|
|
||||||
int value;
|
|
||||||
char *arg;
|
|
||||||
arg = SCmd.next();
|
|
||||||
if (arg != NULL)
|
|
||||||
cmd = atoi(arg);
|
|
||||||
char *val;
|
|
||||||
val = SCmd.next();
|
|
||||||
if (val != NULL)
|
|
||||||
value = atoi(val);
|
|
||||||
if ((arg != NULL) && (val != NULL)){
|
|
||||||
switch (cmd) {
|
|
||||||
case 4: penDownPos= (int) ((float) (value-6000)/(float) 133.3); // transformation from EBB to PWM-Servo
|
|
||||||
storePenDownPosInEE();
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 5: penUpPos= (int)((float) (value-6000)/(float) 133.3); // transformation from EBB to PWM-Servo
|
|
||||||
storePenUpPosInEE();
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 6: //rotMin=value; ignored
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 7: //rotMax=value; ignored
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 11: servoRateUp=value;
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
case 12: servoRateDown=value;
|
|
||||||
sendAck();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendVersion(){
|
|
||||||
Serial.print(initSting);
|
|
||||||
Serial.print("\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void unrecognized(const char *command){
|
|
||||||
sendError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ignore(){
|
|
||||||
sendAck();
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
void initHardware(){
|
|
||||||
// enable eeprom wait in avr/eeprom.h functions
|
|
||||||
SPMCSR &= ~SELFPRGEN;
|
|
||||||
|
|
||||||
loadPenPosFromEE();
|
|
||||||
|
|
||||||
pinMode(enableRotMotor, OUTPUT);
|
|
||||||
pinMode(enablePenMotor, OUTPUT);
|
|
||||||
|
|
||||||
rotMotor.setMaxSpeed(2000.0);
|
|
||||||
rotMotor.setAcceleration(10000.0);
|
|
||||||
penMotor.setMaxSpeed(2000.0);
|
|
||||||
penMotor.setAcceleration(10000.0);
|
|
||||||
motorsOff();
|
|
||||||
penServo.attach(servoPin);
|
|
||||||
penServo.write(penState);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void loadPenPosFromEE() {
|
|
||||||
penUpPos = eeprom_read_word(penUpPosEEAddress);
|
|
||||||
penDownPos = eeprom_read_word(penDownPosEEAddress);
|
|
||||||
penState = penUpPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void storePenUpPosInEE() {
|
|
||||||
eeprom_update_word(penUpPosEEAddress, penUpPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void storePenDownPosInEE() {
|
|
||||||
eeprom_update_word(penDownPosEEAddress, penDownPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void sendAck(){
|
|
||||||
Serial.print("OK\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void sendError(){
|
|
||||||
Serial.print("unknown CMD\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void motorsOff() {
|
|
||||||
digitalWrite(enableRotMotor, HIGH);
|
|
||||||
digitalWrite(enablePenMotor, HIGH);
|
|
||||||
motorsEnabled = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void motorsOn() {
|
|
||||||
digitalWrite(enableRotMotor, LOW) ;
|
|
||||||
digitalWrite(enablePenMotor, LOW) ;
|
|
||||||
motorsEnabled = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleMotors() {
|
|
||||||
if (motorsEnabled) {
|
|
||||||
motorsOff();
|
|
||||||
} else {
|
|
||||||
motorsOn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB) {
|
|
||||||
char *arg1;
|
|
||||||
char *arg2;
|
|
||||||
char *arg3;
|
|
||||||
arg1 = SCmd.next();
|
|
||||||
if (arg1 != NULL) {
|
|
||||||
*duration = atoi(arg1);
|
|
||||||
arg2 = SCmd.next();
|
|
||||||
}
|
|
||||||
if (arg2 != NULL) {
|
|
||||||
*penStepsEBB = atoi(arg2);
|
|
||||||
arg3 = SCmd.next();
|
|
||||||
}
|
|
||||||
if (arg3 != NULL) {
|
|
||||||
*rotStepsEBB = atoi(arg3);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void prepareMove(uint16_t duration, int penStepsEBB, int rotStepsEBB) {
|
|
||||||
if (!motorsEnabled) {
|
|
||||||
motorsOn();
|
|
||||||
}
|
|
||||||
if( (1 == rotStepCorrection) && (1 == penStepCorrection) ){ // if coordinatessystems are identical
|
|
||||||
//set Coordinates and Speed
|
|
||||||
rotMotor.move(rotStepsEBB);
|
|
||||||
rotMotor.setSpeed( abs( (float)rotStepsEBB * (float)1000 / (float)duration ) );
|
|
||||||
penMotor.move(penStepsEBB);
|
|
||||||
penMotor.setSpeed( abs( (float)penStepsEBB * (float)1000 / (float)duration ) );
|
|
||||||
} else {
|
|
||||||
//incoming EBB-Steps will be multiplied by 16, then Integer-maths is done, result will be divided by 16
|
|
||||||
// This make thinks here really complicated, but floating point-math kills performance and memory, believe me... I tried...
|
|
||||||
long rotSteps = ( (long)rotStepsEBB * 16 / rotStepCorrection) + (long)rotStepError; //correct incoming EBB-Steps to our microstep-Setting and multiply by 16 to avoid floatingpoint...
|
|
||||||
long penSteps = ( (long)penStepsEBB * 16 / penStepCorrection) + (long)penStepError;
|
|
||||||
|
|
||||||
int rotStepsToGo = (int) (rotSteps/16); //Calc Steps to go, which are possible on our machine
|
|
||||||
int penStepsToGo = (int) (penSteps/16);
|
|
||||||
|
|
||||||
rotStepError = (long)rotSteps - ((long) rotStepsToGo * (long)16); // calc Position-Error, if there is one
|
|
||||||
penStepError = (long)penSteps - ((long) penStepsToGo * (long)16);
|
|
||||||
|
|
||||||
long temp_rotSpeed = ((long)rotStepsToGo * (long)1000 / (long)duration ); // calc Speed in Integer Math
|
|
||||||
long temp_penSpeed = ((long)penStepsToGo * (long)1000 / (long)duration ) ;
|
|
||||||
|
|
||||||
float rotSpeed= (float) abs(temp_rotSpeed); // type cast
|
|
||||||
float penSpeed= (float) abs(temp_penSpeed);
|
|
||||||
|
|
||||||
//set Coordinates and Speed
|
|
||||||
rotMotor.move(rotStepsToGo); // finally, let us set the target position...
|
|
||||||
rotMotor.setSpeed(rotSpeed); // and the Speed!
|
|
||||||
penMotor.move(penStepsToGo);
|
|
||||||
penMotor.setSpeed( penSpeed );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void moveOneStep() {
|
|
||||||
if ( penMotor.distanceToGo() || rotMotor.distanceToGo() ) {
|
|
||||||
penMotor.runSpeedToPosition(); // Moving.... moving... moving....
|
|
||||||
rotMotor.runSpeedToPosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void moveToDestination() {
|
|
||||||
while ( penMotor.distanceToGo() || rotMotor.distanceToGo() ) {
|
|
||||||
penMotor.runSpeedToPosition(); // Moving.... moving... moving....
|
|
||||||
rotMotor.runSpeedToPosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setprgButtonState(){
|
|
||||||
prgButtonState = 1;
|
|
||||||
}
|
|
||||||
12
README.md
12
README.md
@@ -44,3 +44,15 @@ http://wiki.evilmadscientist.com/Installing_software
|
|||||||
# return serialPort
|
# return serialPort
|
||||||
- In my version lines 1355-1360
|
- In my version lines 1355-1360
|
||||||
|
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Add credentials.h file with content like this:
|
||||||
|
|
||||||
|
const char *kWifiSsid = "MySSID";
|
||||||
|
const char *kWifiPassword = "MySecret";
|
||||||
|
|
||||||
|
To disable Wifi:
|
||||||
|
|
||||||
|
const char *kWifiSsid = 0;
|
||||||
|
const char *kWifiPassword = 0;
|
||||||
|
|
||||||
|
|||||||
46
docs/eggbot-ble-serial-protocol.md
Normal file
46
docs/eggbot-ble-serial-protocol.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# EggBot BLE Serial Protocol
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
This transport exposes the same EggBot command protocol as serial, but over BLE GATT.
|
||||||
|
|
||||||
|
- Protocol commands and responses are unchanged.
|
||||||
|
- Framing is unchanged (`\r` command terminator, `\r\n` response lines).
|
||||||
|
|
||||||
|
## BLE GATT Profile
|
||||||
|
- Device name: `EggDuino`
|
||||||
|
- Service UUID: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
|
- RX characteristic (host -> EggDuino): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
|
- Properties: `Write`, `Write Without Response`
|
||||||
|
- TX characteristic (EggDuino -> host): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
|
- Properties: `Notify`, `Read`
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
- Host writes plain ASCII command bytes to RX.
|
||||||
|
- Firmware parses bytes with the same EggBot command parser used for USB serial.
|
||||||
|
- Firmware sends responses via TX notifications.
|
||||||
|
- Long responses are segmented into BLE-sized notification chunks; host must reassemble by bytes and parse lines by `\r\n`.
|
||||||
|
|
||||||
|
## Compatibility Rules
|
||||||
|
- A BLE client must send commands exactly as serial hosts do.
|
||||||
|
- Each command must end with `\r`.
|
||||||
|
- Data-returning commands (`QP`, `QB`, `QN`, `QL`) return a value line before final status (`OK\r\n`).
|
||||||
|
- Unknown/invalid command format returns `unknown CMD\r\n`.
|
||||||
|
|
||||||
|
## Example BLE Transactions
|
||||||
|
Version query:
|
||||||
|
- Write RX: `v\r`
|
||||||
|
- Notify TX: `EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a\r\n`
|
||||||
|
|
||||||
|
Move command:
|
||||||
|
- Write RX: `SM,100,0,200\r`
|
||||||
|
- Notify TX: `OK\r\n`
|
||||||
|
|
||||||
|
Query node count:
|
||||||
|
- Write RX: `QN\r`
|
||||||
|
- Notify TX: `<number>\r\n`
|
||||||
|
- Notify TX: `OK\r\n`
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
- BLE receive bytes are queued and parsed in the main firmware loop.
|
||||||
|
- If the BLE RX queue overruns, excess bytes are dropped and a log entry is generated.
|
||||||
|
- BLE and USB serial can coexist; each command response is routed to the transport that received that command.
|
||||||
53
docs/eggbot-serial-protocol.md
Normal file
53
docs/eggbot-serial-protocol.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# EggBot Serial Protocol (USB/UART)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
This firmware emulates the EggBot/EBB command protocol over serial transport.
|
||||||
|
|
||||||
|
- Transport: USB CDC/UART (`115200 8N1`)
|
||||||
|
- Command separator: `,`
|
||||||
|
- Command terminator: carriage return (`\r`, ASCII 0x0D)
|
||||||
|
- Response terminator: carriage return + newline (`\r\n`)
|
||||||
|
|
||||||
|
## Handshake and Generic Responses
|
||||||
|
- Version query: `v\r`
|
||||||
|
- Version response: `EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a\r\n`
|
||||||
|
- Success response: `OK\r\n`
|
||||||
|
- Error response: `unknown CMD\r\n`
|
||||||
|
|
||||||
|
## Implemented Commands
|
||||||
|
All commands are case-sensitive and comma-delimited.
|
||||||
|
|
||||||
|
- `EM,<mode>[,<ignored>]`
|
||||||
|
- `SC,<id>,<value>`
|
||||||
|
- `SP,<penState>[,<delayMs>]`
|
||||||
|
- `SM,<durationMs>,<penSteps>,<rotSteps>`
|
||||||
|
- `SE` (ack-only placeholder)
|
||||||
|
- `TP[,<delayMs>]`
|
||||||
|
- `PO` (ack-only placeholder)
|
||||||
|
- `NI`
|
||||||
|
- `ND`
|
||||||
|
- `SN,<nodeCount>`
|
||||||
|
- `QN`
|
||||||
|
- `SL,<layer>`
|
||||||
|
- `QL`
|
||||||
|
- `QP`
|
||||||
|
- `QB`
|
||||||
|
|
||||||
|
## Command Behavior Notes
|
||||||
|
- `SM`: firmware blocks until previous move completes, sends `OK`, then runs the requested move.
|
||||||
|
- `SP` and `TP`: support optional delay argument in milliseconds.
|
||||||
|
- `QP`: returns pen state line (`1` for pen up, `0` for pen down), then `OK`.
|
||||||
|
- `QB`: returns button state (`0` or `1`), then `OK`; internal button latch is reset after query.
|
||||||
|
- `QN`, `QL`: return numeric line first, then `OK`.
|
||||||
|
- `SC` supports these IDs:
|
||||||
|
- `4`: set pen-down servo position (EBB value to servo-mapped value)
|
||||||
|
- `5`: set pen-up servo position
|
||||||
|
- `6`, `7`: accepted, ignored, and acknowledged
|
||||||
|
- `11`: set servo rate up
|
||||||
|
- `12`: set servo rate down
|
||||||
|
|
||||||
|
## Timing/Parsing Requirements for Hosts
|
||||||
|
- Always terminate each command with `\r`.
|
||||||
|
- Do not rely on `\n` as a command terminator.
|
||||||
|
- Read until `OK\r\n` (or `unknown CMD\r\n`) to complete command transactions.
|
||||||
|
- For commands that return data (`QP`, `QB`, `QN`, `QL`), read one data line plus the final status line.
|
||||||
45
docs/eggbot-wifi-serial-protocol.md
Normal file
45
docs/eggbot-wifi-serial-protocol.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# EggBot WiFi Serial Protocol
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
This transport exposes the same EggBot command protocol as serial, but over WiFi/WebSocket.
|
||||||
|
|
||||||
|
- Protocol commands and responses are unchanged.
|
||||||
|
- Framing is unchanged (`\r` command terminator, `\r\n` response lines).
|
||||||
|
|
||||||
|
## Transport Profile
|
||||||
|
- Protocol endpoint: `ws://<eggs-esp32-ip>:1337/`
|
||||||
|
- WebSocket message type: text or binary frames accepted
|
||||||
|
- Payload: ASCII command bytes
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
- Host writes command bytes to the WebSocket.
|
||||||
|
- Firmware parses incoming bytes with the same EggBot parser used for USB serial and BLE.
|
||||||
|
- Firmware sends responses as WebSocket text messages.
|
||||||
|
- Hosts must treat inbound data as a stream and parse lines by `\r\n`.
|
||||||
|
|
||||||
|
## Compatibility Rules
|
||||||
|
- A WiFi client must send commands exactly as serial hosts do.
|
||||||
|
- Each command must end with `\r`.
|
||||||
|
- Data-returning commands (`QP`, `QB`, `QN`, `QL`) return a value line before final status (`OK\r\n`).
|
||||||
|
- Unknown/invalid command format returns `unknown CMD\r\n`.
|
||||||
|
|
||||||
|
## Example WiFi Transactions
|
||||||
|
Version query:
|
||||||
|
- Send: `v\r`
|
||||||
|
- Receive: `EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a\r\n`
|
||||||
|
|
||||||
|
Move command:
|
||||||
|
- Send: `SM,100,0,200\r`
|
||||||
|
- Receive: `OK\r\n`
|
||||||
|
|
||||||
|
Query layer:
|
||||||
|
- Send: `QL\r`
|
||||||
|
- Receive: `<layer>\r\n`
|
||||||
|
- Receive: `OK\r\n`
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
- The WiFi protocol endpoint is started only when ESP32 station WiFi is connected.
|
||||||
|
- Incoming WiFi bytes are queued and parsed in the main loop.
|
||||||
|
- If the WiFi RX queue overruns, excess bytes are dropped and a log entry is generated.
|
||||||
|
- WiFi transport is single active client: first connected sender is accepted until disconnect.
|
||||||
|
- USB serial, BLE, and WiFi can coexist; responses are routed to the transport that received each command.
|
||||||
185
docs/examples/eggbot-ble-client.mjs
Normal file
185
docs/examples/eggbot-ble-client.mjs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
const SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
|
||||||
|
const RX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
|
||||||
|
const TX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
let device = null;
|
||||||
|
let server = null;
|
||||||
|
let rxCharacteristic = null;
|
||||||
|
let txCharacteristic = null;
|
||||||
|
let lineBuffer = '';
|
||||||
|
let commandQueue = Promise.resolve();
|
||||||
|
|
||||||
|
const lineListeners = new Set();
|
||||||
|
|
||||||
|
function emitLine(line) {
|
||||||
|
for (const listener of lineListeners) {
|
||||||
|
listener(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNotification(event) {
|
||||||
|
const dataView = event.target.value;
|
||||||
|
const chunk = decoder.decode(dataView, { stream: true });
|
||||||
|
lineBuffer += chunk;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const delimiterIndex = lineBuffer.indexOf('\r\n');
|
||||||
|
if (delimiterIndex < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = lineBuffer.slice(0, delimiterIndex);
|
||||||
|
lineBuffer = lineBuffer.slice(delimiterIndex + 2);
|
||||||
|
emitLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisconnected() {
|
||||||
|
server = null;
|
||||||
|
rxCharacteristic = null;
|
||||||
|
if (txCharacteristic) {
|
||||||
|
txCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification);
|
||||||
|
}
|
||||||
|
txCharacteristic = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeToRxCharacteristic(payloadBytes) {
|
||||||
|
if (!rxCharacteristic) {
|
||||||
|
throw new Error('EggDuino is not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rxCharacteristic.writeValueWithoutResponse === 'function') {
|
||||||
|
await rxCharacteristic.writeValueWithoutResponse(payloadBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rxCharacteristic.writeValueWithResponse === 'function') {
|
||||||
|
await rxCharacteristic.writeValueWithResponse(payloadBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await rxCharacteristic.writeValue(payloadBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCommand(command) {
|
||||||
|
return command.endsWith('\r') ? command : `${command}\r`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueCommand(fn) {
|
||||||
|
const run = commandQueue.then(fn, fn);
|
||||||
|
commandQueue = run.catch(() => {});
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectEggDuino() {
|
||||||
|
if (server?.connected && rxCharacteristic && txCharacteristic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [SERVICE_UUID] }],
|
||||||
|
optionalServices: [SERVICE_UUID],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`BLE request failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
device.addEventListener('gattserverdisconnected', handleDisconnected);
|
||||||
|
|
||||||
|
try {
|
||||||
|
server = await device.gatt.connect();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`BLE gatt connect failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let service;
|
||||||
|
try {
|
||||||
|
service = await server.getPrimaryService(SERVICE_UUID);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`BLE service lookup failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rxCharacteristic = await service.getCharacteristic(RX_UUID);
|
||||||
|
txCharacteristic = await service.getCharacteristic(TX_UUID);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`BLE characteristic lookup failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await txCharacteristic.startNotifications();
|
||||||
|
txCharacteristic.addEventListener('characteristicvaluechanged', handleNotification);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`BLE notifications setup failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnectEggDuino() {
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txCharacteristic) {
|
||||||
|
txCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.gatt?.connected) {
|
||||||
|
device.gatt.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEggBotCommand(command) {
|
||||||
|
const normalized = normalizeCommand(command);
|
||||||
|
await writeToRxCharacteristic(encoder.encode(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEggBotCommandExpectOk(command, timeoutMs = 2000) {
|
||||||
|
return enqueueCommand(() => new Promise(async (resolve, reject) => {
|
||||||
|
const collectedLines = [];
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
reject(new Error('EggBot response timeout'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const onResponseLine = (line) => {
|
||||||
|
collectedLines.push(line);
|
||||||
|
|
||||||
|
if (line === 'OK') {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
resolve(collectedLines.slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line === 'unknown CMD') {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
reject(new Error(`EggBot error: ${collectedLines.join(' | ')}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lineListeners.add(onResponseLine);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendEggBotCommand(command);
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onLine(callback) {
|
||||||
|
lineListeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offLine(callback) {
|
||||||
|
lineListeners.delete(callback);
|
||||||
|
}
|
||||||
183
docs/examples/eggbot-wifi-client.mjs
Normal file
183
docs/examples/eggbot-wifi-client.mjs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
let socket = null;
|
||||||
|
let lineBuffer = '';
|
||||||
|
let commandQueue = Promise.resolve();
|
||||||
|
|
||||||
|
const lineListeners = new Set();
|
||||||
|
|
||||||
|
function emitLine(line) {
|
||||||
|
for (const listener of lineListeners) {
|
||||||
|
listener(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIncomingChunk(chunk) {
|
||||||
|
lineBuffer += chunk;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const delimiterIndex = lineBuffer.indexOf('\r\n');
|
||||||
|
if (delimiterIndex < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = lineBuffer.slice(0, delimiterIndex);
|
||||||
|
lineBuffer = lineBuffer.slice(delimiterIndex + 2);
|
||||||
|
emitLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSocketMessage(event) {
|
||||||
|
if (typeof event.data === 'string') {
|
||||||
|
parseIncomingChunk(event.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data instanceof ArrayBuffer) {
|
||||||
|
parseIncomingChunk(decoder.decode(event.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCommand(command) {
|
||||||
|
return command.endsWith('\r') ? command : `${command}\r`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueCommand(fn) {
|
||||||
|
const run = commandQueue.then(fn, fn);
|
||||||
|
commandQueue = run.catch(() => {});
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSocketReady() {
|
||||||
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error('EggDuino WiFi socket is not connected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSocketUrl(options) {
|
||||||
|
if (options.url) {
|
||||||
|
return options.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = options.host;
|
||||||
|
if (!host) {
|
||||||
|
throw new Error('Missing EggDuino host');
|
||||||
|
}
|
||||||
|
|
||||||
|
const secure = options.secure === true;
|
||||||
|
const protocol = secure ? 'wss' : 'ws';
|
||||||
|
const port = Number(options.port ?? 1337);
|
||||||
|
const path = options.path ?? '/';
|
||||||
|
|
||||||
|
return `${protocol}://${host}:${port}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectEggDuinoWifi(options = {}) {
|
||||||
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket && socket.readyState === WebSocket.CONNECTING) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const onOpen = () => {
|
||||||
|
socket.removeEventListener('error', onError);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const onError = () => {
|
||||||
|
socket.removeEventListener('open', onOpen);
|
||||||
|
reject(new Error('EggDuino WiFi socket failed while connecting'));
|
||||||
|
};
|
||||||
|
socket.addEventListener('open', onOpen, { once: true });
|
||||||
|
socket.addEventListener('error', onError, { once: true });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildSocketUrl(options);
|
||||||
|
socket = new WebSocket(url);
|
||||||
|
socket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
socket.addEventListener('message', handleSocketMessage);
|
||||||
|
socket.addEventListener('close', () => {
|
||||||
|
socket = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const onOpen = () => {
|
||||||
|
socket.removeEventListener('error', onError);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = () => {
|
||||||
|
socket.removeEventListener('open', onOpen);
|
||||||
|
socket?.close();
|
||||||
|
socket = null;
|
||||||
|
reject(new Error(`EggDuino WiFi socket connection failed: ${url}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.addEventListener('open', onOpen, { once: true });
|
||||||
|
socket.addEventListener('error', onError, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnectEggDuinoWifi() {
|
||||||
|
if (!socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.removeEventListener('message', handleSocketMessage);
|
||||||
|
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEggBotCommand(command) {
|
||||||
|
ensureSocketReady();
|
||||||
|
socket.send(normalizeCommand(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEggBotCommandExpectOk(command, timeoutMs = 2000) {
|
||||||
|
return enqueueCommand(() => new Promise(async (resolve, reject) => {
|
||||||
|
const collectedLines = [];
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
reject(new Error('EggBot response timeout'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const onResponseLine = (line) => {
|
||||||
|
collectedLines.push(line);
|
||||||
|
|
||||||
|
if (line === 'OK') {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
resolve(collectedLines.slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line === 'unknown CMD') {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
reject(new Error(`EggBot error: ${collectedLines.join(' | ')}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lineListeners.add(onResponseLine);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendEggBotCommand(command);
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
lineListeners.delete(onResponseLine);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onLine(callback) {
|
||||||
|
lineListeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offLine(callback) {
|
||||||
|
lineListeners.delete(callback);
|
||||||
|
}
|
||||||
BIN
firmware/boot_app0.bin
Normal file
BIN
firmware/boot_app0.bin
Normal file
Binary file not shown.
BIN
firmware/bootloader.bin
Normal file
BIN
firmware/bootloader.bin
Normal file
Binary file not shown.
BIN
firmware/firmware.bin
Normal file
BIN
firmware/firmware.bin
Normal file
Binary file not shown.
BIN
firmware/firmware.elf
Executable file
BIN
firmware/firmware.elf
Executable file
Binary file not shown.
131335
firmware/firmware.map
Normal file
131335
firmware/firmware.map
Normal file
File diff suppressed because one or more lines are too long
28
firmware/manifest.json
Normal file
28
firmware/manifest.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "EggDuino ESP32 Firmware",
|
||||||
|
"version": "1.6a",
|
||||||
|
"new_install_prompt_erase": true,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"chipFamily": "ESP32",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"path": "bootloader.bin",
|
||||||
|
"offset": 4096
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "partitions.bin",
|
||||||
|
"offset": 32768
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "boot_app0.bin",
|
||||||
|
"offset": 57344
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "firmware.bin",
|
||||||
|
"offset": 65536
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
firmware/partitions.bin
Normal file
BIN
firmware/partitions.bin
Normal file
Binary file not shown.
182
include/EggDuino.h
Normal file
182
include/EggDuino.h
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#ifndef EGGDUINO_H
|
||||||
|
#define EGGDUINO_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#define SUPPRESS_HPP_WARNING
|
||||||
|
#include <ServoEasing.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <SPIFFS.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <FastAccelStepper.h>
|
||||||
|
#include "SerialCommand.h"
|
||||||
|
#include "button.h"
|
||||||
|
|
||||||
|
// implemented Eggbot-Protocol-Version v13
|
||||||
|
#define initSting "EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a"
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
|
||||||
|
// Rotational Stepper
|
||||||
|
static const int kDefaultRotDirPin = 16;
|
||||||
|
static const int kDefaultRotEnablePin = 12;
|
||||||
|
static const int kDefaultRotStepPin = 26;
|
||||||
|
#define rotMicrostep 32
|
||||||
|
static const int kDefaultRotMicrostep = rotMicrostep;
|
||||||
|
|
||||||
|
// Pen Stepper
|
||||||
|
static const int kDefaultPenStepPin = 25;
|
||||||
|
static const int kDefaultPenDirPin = 27;
|
||||||
|
static const int kDefaultPenEnablePin = 12;
|
||||||
|
#define penMicrostep 32
|
||||||
|
static const int kDefaultPenMicrostep = penMicrostep;
|
||||||
|
|
||||||
|
static const int kDefaultServoPin = 17;
|
||||||
|
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// Rotational Stepper
|
||||||
|
static const int kDefaultRotStepPin = 2;
|
||||||
|
static const int kDefaultRotDirPin = 5;
|
||||||
|
static const int kDefaultRotEnablePin = 8;
|
||||||
|
#define rotMicrostep 16
|
||||||
|
static const int kDefaultRotMicrostep = rotMicrostep;
|
||||||
|
|
||||||
|
// Pen Stepper
|
||||||
|
static const int kDefaultPenStepPin = 3;
|
||||||
|
static const int kDefaultPenDirPin = 6;
|
||||||
|
static const int kDefaultPenEnablePin = 8;
|
||||||
|
#define penMicrostep 16
|
||||||
|
static const int kDefaultPenMicrostep = penMicrostep;
|
||||||
|
|
||||||
|
static const int kDefaultServoPin = 4;
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct ConfigParameter {
|
||||||
|
const char *type;
|
||||||
|
const char *key;
|
||||||
|
void *value;
|
||||||
|
String description;
|
||||||
|
int defaultValue;
|
||||||
|
const char *defaultText;
|
||||||
|
bool secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern FastAccelStepperEngine g_stepEngine;
|
||||||
|
extern FastAccelStepper *g_pStepperRotate;
|
||||||
|
extern FastAccelStepper *g_pStepperPen;
|
||||||
|
|
||||||
|
extern ServoEasing penServo;
|
||||||
|
extern SerialCommand SCmd;
|
||||||
|
#ifdef ESP32
|
||||||
|
extern SerialCommand g_BLECmd;
|
||||||
|
extern SerialCommand g_WifiCmd;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern int g_iPenUpPos;
|
||||||
|
extern int g_iPenDownPos;
|
||||||
|
extern int g_iServoRateUp;
|
||||||
|
extern int g_iServoRateDown;
|
||||||
|
extern long g_iRotStepError;
|
||||||
|
extern long g_iPenStepError;
|
||||||
|
extern int g_iPenState;
|
||||||
|
extern uint32_t g_uiNodeCount;
|
||||||
|
extern unsigned int g_uiLayer;
|
||||||
|
extern boolean g_bPrgButtonState;
|
||||||
|
extern float fROT_STEP_CORRECTION;
|
||||||
|
extern float fPEN_STEP_CORRECTION;
|
||||||
|
extern boolean g_bMotorsEnabled;
|
||||||
|
extern int g_iRotDirPin;
|
||||||
|
extern int g_iRotEnablePin;
|
||||||
|
extern int g_iRotStepPin;
|
||||||
|
extern int g_iPenStepPin;
|
||||||
|
extern int g_iPenDirPin;
|
||||||
|
extern int g_iPenEnablePin;
|
||||||
|
extern int g_iServoPin;
|
||||||
|
extern int g_iRotMicrostep;
|
||||||
|
extern int g_iPenMicrostep;
|
||||||
|
extern int g_iMaxAcclSpeed;
|
||||||
|
extern String g_sHostname;
|
||||||
|
extern String g_sWifiSsid;
|
||||||
|
extern String g_sWifiPassword;
|
||||||
|
|
||||||
|
enum ProtocolTransport {
|
||||||
|
PROTOCOL_TRANSPORT_SERIAL = 0,
|
||||||
|
PROTOCOL_TRANSPORT_BLE = 1,
|
||||||
|
PROTOCOL_TRANSPORT_WIFI = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
extern ConfigParameter configParameters[];
|
||||||
|
extern const size_t configParameterCount;
|
||||||
|
|
||||||
|
void makeComInterface();
|
||||||
|
void setActiveProtocolContext(SerialCommand *parser, ProtocolTransport transport);
|
||||||
|
char *nextCommandArg();
|
||||||
|
void protocolWrite(const char *message);
|
||||||
|
void protocolWrite(const String &message);
|
||||||
|
void initHardware();
|
||||||
|
void moveOneStep();
|
||||||
|
void moveToDestination();
|
||||||
|
void sendAck();
|
||||||
|
void sendError();
|
||||||
|
void motorsOff();
|
||||||
|
void motorsOn();
|
||||||
|
void toggleMotors();
|
||||||
|
void doTogglePen();
|
||||||
|
void setprgButtonState();
|
||||||
|
bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB);
|
||||||
|
void prepareMove(uint16_t duration, int penStepsEBB, int rotStepsEBB);
|
||||||
|
void storePenUpPosInEE();
|
||||||
|
void storePenDownPosInEE();
|
||||||
|
void updateStepCorrectionFactors();
|
||||||
|
void movePenServoTo(int targetPos);
|
||||||
|
|
||||||
|
bool initConfigStore();
|
||||||
|
bool loadConfigFromFile();
|
||||||
|
bool saveConfigToFile();
|
||||||
|
String buildConfigJson();
|
||||||
|
bool applyConfigJson(const String &payload, String &errorMessage);
|
||||||
|
void startWebInterface();
|
||||||
|
void handleWebInterface();
|
||||||
|
#ifdef ESP32
|
||||||
|
void startBleInterface();
|
||||||
|
void handleBleInterface();
|
||||||
|
bool bleProtocolWrite(const char *message);
|
||||||
|
void buildDeviceName(char *nameBuffer, size_t bufferSize);
|
||||||
|
void startWifiProtocolInterface();
|
||||||
|
void handleWifiProtocolInterface();
|
||||||
|
bool wifiProtocolWrite(const char *message);
|
||||||
|
#else
|
||||||
|
inline void startBleInterface() {}
|
||||||
|
inline void handleBleInterface() {}
|
||||||
|
inline bool bleProtocolWrite(const char *message)
|
||||||
|
{
|
||||||
|
(void)message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inline void buildDeviceName(char *nameBuffer, size_t bufferSize)
|
||||||
|
{
|
||||||
|
(void)nameBuffer;
|
||||||
|
(void)bufferSize;
|
||||||
|
}
|
||||||
|
inline void startWifiProtocolInterface() {}
|
||||||
|
inline void handleWifiProtocolInterface() {}
|
||||||
|
inline bool wifiProtocolWrite(const char *message)
|
||||||
|
{
|
||||||
|
(void)message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
void Log(const String &message);
|
||||||
|
void Log(const char *message);
|
||||||
|
String buildLogsJson(uint32_t sinceSeq);
|
||||||
|
void captureIncomingPrintLine(const char *line, ProtocolTransport transport);
|
||||||
|
String buildIncomingPrintLogText();
|
||||||
|
void clearIncomingPrintLog();
|
||||||
|
|
||||||
|
#endif
|
||||||
51
include/motion_planner.h
Normal file
51
include/motion_planner.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <FastAccelStepper.h>
|
||||||
|
|
||||||
|
class XYMotionPlanner {
|
||||||
|
public:
|
||||||
|
XYMotionPlanner(FastAccelStepper* stepperX, FastAccelStepper* stepperY);
|
||||||
|
|
||||||
|
// Basisparameter der "dominanten" Achse
|
||||||
|
void setBaseLimits(uint32_t maxSpeedHz, uint32_t maxAccelStepsPerS2);
|
||||||
|
|
||||||
|
// Minimale Werte vermeiden, dass eine kurze Achse mit 0 endet
|
||||||
|
void setMinimums(uint32_t minSpeedHz, uint32_t minAccelStepsPerS2);
|
||||||
|
|
||||||
|
// Aktuelle Positionen der Planner-Sicht setzen, z.B. nach Homing
|
||||||
|
void setCurrentPosition(int32_t x, int32_t y);
|
||||||
|
|
||||||
|
int32_t currentX() const;
|
||||||
|
int32_t currentY() const;
|
||||||
|
|
||||||
|
bool isRunning() const;
|
||||||
|
|
||||||
|
// Optional blockierend warten
|
||||||
|
void waitUntilFinished();
|
||||||
|
|
||||||
|
// Startet eine koordinierte lineare P2P-Bewegung
|
||||||
|
// Rückgabe false = nichts zu tun oder ungültig
|
||||||
|
bool moveTo(int32_t targetX, int32_t targetY);
|
||||||
|
|
||||||
|
// Convenience: relative Bewegung
|
||||||
|
bool moveBy(int32_t deltaX, int32_t deltaY);
|
||||||
|
|
||||||
|
// Muss regelmäßig aufgerufen werden, wenn du die Planner-Position
|
||||||
|
// nach abgeschlossener Bewegung sauber nachführen willst
|
||||||
|
void update();
|
||||||
|
|
||||||
|
private:
|
||||||
|
FastAccelStepper* m_x = nullptr;
|
||||||
|
FastAccelStepper* m_y = nullptr;
|
||||||
|
|
||||||
|
int32_t m_currentX = 0;
|
||||||
|
int32_t m_currentY = 0;
|
||||||
|
int32_t m_targetX = 0;
|
||||||
|
int32_t m_targetY = 0;
|
||||||
|
|
||||||
|
uint32_t m_baseSpeedHz = 2000;
|
||||||
|
uint32_t m_baseAccel = 4000;
|
||||||
|
uint32_t m_minSpeedHz = 1;
|
||||||
|
uint32_t m_minAccel = 1;
|
||||||
|
|
||||||
|
static uint32_t scaledValue(uint32_t base, float scale, uint32_t distance);
|
||||||
|
};
|
||||||
BIN
inkscape/linux/AxiDraw_395_LinX86.zip
Normal file
BIN
inkscape/linux/AxiDraw_395_LinX86.zip
Normal file
Binary file not shown.
452
inkscape/linux/patch/ebb_serial.py
Normal file
452
inkscape/linux/patch/ebb_serial.py
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
'''
|
||||||
|
ebb_serial.py
|
||||||
|
Serial connection utilities for EiBotBoard
|
||||||
|
https://github.com/evil-mad/plotink
|
||||||
|
|
||||||
|
Intended to provide some common interfaces that can be used by
|
||||||
|
EggBot, WaterColorBot, AxiDraw, and similar machines.
|
||||||
|
|
||||||
|
See below for version information
|
||||||
|
|
||||||
|
Thanks to Shel Michaels for bug fixes and helpful suggestions.
|
||||||
|
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 Windell H. Oskay, Evil Mad Scientist Laboratories
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from packaging.version import parse
|
||||||
|
|
||||||
|
from .plot_utils_import import from_dependency_import
|
||||||
|
inkex = from_dependency_import('ink_extensions.inkex')
|
||||||
|
serial = from_dependency_import('serial')
|
||||||
|
from serial.tools.list_ports import comports \
|
||||||
|
#pylint: disable=wrong-import-position, wrong-import-order
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def version():
|
||||||
|
'''Version number for this document'''
|
||||||
|
return "0.19" # Dated 2022-10-05
|
||||||
|
|
||||||
|
|
||||||
|
def findPort():
|
||||||
|
'''
|
||||||
|
Find first available EiBotBoard by searching USB ports. Return serial port object.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
com_ports_list = list(comports())
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
ebb_port = None
|
||||||
|
for port in com_ports_list:
|
||||||
|
if port[0].startswith("/dev/ttyUSB"):
|
||||||
|
ebb_port = port[0] # Success; EBB found by name match.
|
||||||
|
break # stop searching-- we are done.
|
||||||
|
for port in com_ports_list:
|
||||||
|
if port[1].startswith("EiBotBoard"):
|
||||||
|
ebb_port = port[0] # Success; EBB found by name match.
|
||||||
|
break # stop searching-- we are done.
|
||||||
|
if ebb_port is None:
|
||||||
|
for port in com_ports_list:
|
||||||
|
if port[2].startswith("USB VID:PID=04D8:FD92"):
|
||||||
|
ebb_port = port[0] # Success; EBB found by VID/PID match.
|
||||||
|
break # stop searching-- we are done.
|
||||||
|
return ebb_port
|
||||||
|
|
||||||
|
|
||||||
|
def find_named_ebb(port_name):
|
||||||
|
'''
|
||||||
|
Find a specific EiBotBoard identified by a string giving either:
|
||||||
|
The enumerated serial port, or
|
||||||
|
An EBB "Name tag"
|
||||||
|
Names should be 3-16 characters long. Comparisons are not case sensitive.
|
||||||
|
(Name tags may assigned with the ST command on firmware 2.5.5 and later.)
|
||||||
|
If found: Return serial port name (enumeration)
|
||||||
|
If not found, Return None
|
||||||
|
'''
|
||||||
|
if port_name is not None:
|
||||||
|
needle = 'SER=' + port_name # pyserial 3
|
||||||
|
needle2 = 'SNR=' + port_name # pyserial 2.7
|
||||||
|
needle3 = '(' + port_name + ')' # e.g., "(COM4)"
|
||||||
|
|
||||||
|
needle = needle.lower()
|
||||||
|
needle2 = needle2.lower()
|
||||||
|
needle3 = needle3.lower()
|
||||||
|
plower = port_name.lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
com_ports_list = list(comports())
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for port in com_ports_list:
|
||||||
|
p_0 = port[0].lower()
|
||||||
|
p_1 = port[1].lower()
|
||||||
|
p_2 = port[2].lower()
|
||||||
|
|
||||||
|
if needle in p_2:
|
||||||
|
return port[0] # Success; EBB found by name match.
|
||||||
|
if needle2 in p_2:
|
||||||
|
return port[0] # Success; EBB found by name match.
|
||||||
|
if needle3 in p_1:
|
||||||
|
return port[0] # Success; EBB found by port match.
|
||||||
|
|
||||||
|
p_1 = p_1[11:]
|
||||||
|
if p_1.startswith(plower):
|
||||||
|
return port[0] # Success; EBB found by name match.
|
||||||
|
if p_0.startswith(plower):
|
||||||
|
return port[0] # Success; EBB found by port match.
|
||||||
|
|
||||||
|
needle.replace(" ", "_") # SN on Windows has underscores, not spaces.
|
||||||
|
if needle in p_2:
|
||||||
|
return port[0] # Success; EBB found by port match.
|
||||||
|
|
||||||
|
needle2.replace(" ", "_") # SN on Windows has underscores, not spaces.
|
||||||
|
if needle2 in p_2:
|
||||||
|
return port[0] # Success; EBB found by port match.
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def query_nickname(port_name, verbose=True):
|
||||||
|
'''
|
||||||
|
Query the EBB nickname and report it.
|
||||||
|
If verbose is True or omitted, the result will be human readable.
|
||||||
|
A short version is returned if verbose is False.
|
||||||
|
Requires firmware version 2.5.5 or newer. http://evil-mad.github.io/EggBot/ebb.html#QT
|
||||||
|
'''
|
||||||
|
if port_name is not None:
|
||||||
|
version_status = min_version(port_name, "2.5.5")
|
||||||
|
|
||||||
|
if version_status:
|
||||||
|
raw_string = (query(port_name, 'QT\r'))
|
||||||
|
if raw_string.isspace():
|
||||||
|
if verbose:
|
||||||
|
return "This AxiDraw does not have a nickname assigned."
|
||||||
|
return None
|
||||||
|
if verbose:
|
||||||
|
return "AxiDraw nickname: " + raw_string
|
||||||
|
return str(raw_string).strip()
|
||||||
|
if version_status is False:
|
||||||
|
if verbose:
|
||||||
|
return "AxiDraw naming requires firmware version 2.5.5 or higher."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def write_nickname(port_name, nickname):
|
||||||
|
'''
|
||||||
|
Write the EBB nickname.
|
||||||
|
Requires firmware version 2.5.5 or newer. http://evil-mad.github.io/EggBot/ebb.html#ST
|
||||||
|
'''
|
||||||
|
if port_name is not None:
|
||||||
|
version_status = min_version(port_name, "2.5.5")
|
||||||
|
|
||||||
|
if version_status:
|
||||||
|
try:
|
||||||
|
cmd = 'ST,' + nickname + '\r'
|
||||||
|
command(port_name,cmd)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def reboot(port_name):
|
||||||
|
'''
|
||||||
|
Reboot the EBB, as though it were just powered on.
|
||||||
|
Requires firmware version 2.5.5 or newer. http://evil-mad.github.io/EggBot/ebb.html#RB
|
||||||
|
'''
|
||||||
|
if port_name is not None:
|
||||||
|
version_status = min_version(port_name, "2.5.5")
|
||||||
|
if version_status:
|
||||||
|
try:
|
||||||
|
command(port_name,'RB\r')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def list_port_info():
|
||||||
|
'''Find and return a list of all USB devices and their information.'''
|
||||||
|
try:
|
||||||
|
com_ports_list = list(comports())
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
port_info_list = []
|
||||||
|
for port in com_ports_list:
|
||||||
|
port_info_list.append(port[0]) # port name
|
||||||
|
port_info_list.append(port[1]) # Identifier
|
||||||
|
port_info_list.append(port[2]) # VID/PID
|
||||||
|
if port_info_list:
|
||||||
|
return port_info_list
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def listEBBports():
|
||||||
|
'''Find and return a list of all EiBotBoard units connected via USB port.'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
com_ports_list = list(comports())
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
ebb_ports_list = []
|
||||||
|
for port in com_ports_list:
|
||||||
|
port_has_ebb = False
|
||||||
|
if port[0].startswith("/dev/ttyUSB"):
|
||||||
|
port_has_ebb = True
|
||||||
|
elif port[1].startswith("EiBotBoard"):
|
||||||
|
port_has_ebb = True
|
||||||
|
elif port[2].startswith("USB VID:PID=04D8:FD92"):
|
||||||
|
port_has_ebb = True
|
||||||
|
if port_has_ebb:
|
||||||
|
ebb_ports_list.append(port)
|
||||||
|
if ebb_ports_list:
|
||||||
|
return ebb_ports_list
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_named_ebbs():
|
||||||
|
'''Return descriptive list of all EiBotBoard units'''
|
||||||
|
ebb_ports_list = listEBBports()
|
||||||
|
if not ebb_ports_list:
|
||||||
|
return None
|
||||||
|
ebb_names_list = []
|
||||||
|
for port in ebb_ports_list:
|
||||||
|
name_found = False
|
||||||
|
p_0 = port[0]
|
||||||
|
p_1 = port[1]
|
||||||
|
p_2 = port[2]
|
||||||
|
if p_1.startswith("EiBotBoard"):
|
||||||
|
temp_string = p_1[11:]
|
||||||
|
if temp_string:
|
||||||
|
if temp_string is not None:
|
||||||
|
ebb_names_list.append(temp_string)
|
||||||
|
name_found = True
|
||||||
|
if not name_found:
|
||||||
|
# Look for "SER=XXXX LOCAT" pattern,
|
||||||
|
# typical of Pyserial 3 on Windows.
|
||||||
|
if 'SER=' in p_2 and ' LOCAT' in p_2:
|
||||||
|
index1 = p_2.find('SER=') + len('SER=')
|
||||||
|
index2 = p_2.find(' LOCAT', index1)
|
||||||
|
temp_string = p_2[index1:index2]
|
||||||
|
if len(temp_string) < 3:
|
||||||
|
temp_string = None
|
||||||
|
if temp_string is not None:
|
||||||
|
ebb_names_list.append(temp_string)
|
||||||
|
name_found = True
|
||||||
|
if not name_found:
|
||||||
|
# Look for "...SNR=XXXX" pattern,
|
||||||
|
# typical of Pyserial 2.7 on Windows
|
||||||
|
if 'SNR=' in p_2:
|
||||||
|
index1 = p_2.find('SNR=') + len('SNR=')
|
||||||
|
index2 = len(p_2)
|
||||||
|
temp_string = p_2[index1:index2]
|
||||||
|
if len(temp_string) < 3:
|
||||||
|
temp_string = None
|
||||||
|
if temp_string is not None:
|
||||||
|
ebb_names_list.append(temp_string)
|
||||||
|
name_found = True
|
||||||
|
if not name_found:
|
||||||
|
ebb_names_list.append(p_0)
|
||||||
|
return ebb_names_list
|
||||||
|
|
||||||
|
|
||||||
|
def testPort(port_name):
|
||||||
|
"""
|
||||||
|
Open a given serial port, verify that it is an EiBotBoard,
|
||||||
|
and return a SerialPort object that we can reference later.
|
||||||
|
|
||||||
|
This routine only opens the port; it will need to be closed as well,
|
||||||
|
for example with closePort( port_name ).
|
||||||
|
You, who open the port, are responsible for closing it as well.
|
||||||
|
"""
|
||||||
|
if port_name is not None:
|
||||||
|
try:
|
||||||
|
serial_port = serial.Serial(port_name, timeout=1.0, baudrate=115200) # 1 second timeout!
|
||||||
|
|
||||||
|
serial_port.flushInput() # deprecated function name;
|
||||||
|
# use serial_port.reset_input_buffer()
|
||||||
|
# if we can be sure that we have pySerial 3+.
|
||||||
|
|
||||||
|
serial_port.write('v\r'.encode('ascii'))
|
||||||
|
str_version = serial_port.readline()
|
||||||
|
if str_version and str_version.startswith("EBB".encode('ascii')):
|
||||||
|
return serial_port
|
||||||
|
|
||||||
|
serial_port.write('v\r'.encode('ascii'))
|
||||||
|
str_version = serial_port.readline()
|
||||||
|
if str_version and str_version.startswith("EBB".encode('ascii')):
|
||||||
|
return serial_port
|
||||||
|
serial_port.close()
|
||||||
|
except serial.SerialException as err:
|
||||||
|
logger.error("Error testing serial port `{}` connection".format(port_name))
|
||||||
|
logger.info("Error context:", exc_info=err)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def openPort():
|
||||||
|
'''
|
||||||
|
Find and open a port to a single attached EiBotBoard.
|
||||||
|
The first port located will be used.
|
||||||
|
'''
|
||||||
|
found_port = findPort()
|
||||||
|
serial_port = testPort(found_port)
|
||||||
|
if serial_port:
|
||||||
|
return serial_port
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def open_named_port(port_name):
|
||||||
|
'''
|
||||||
|
Find and open a port to a single attached EiBotBoard, indicated by name.
|
||||||
|
The first port located will be used.
|
||||||
|
'''
|
||||||
|
found_port = find_named_ebb(port_name)
|
||||||
|
serial_port = testPort(found_port)
|
||||||
|
if serial_port:
|
||||||
|
return serial_port
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def closePort(port_name):
|
||||||
|
'''Close the given serial port.'''
|
||||||
|
if port_name is not None:
|
||||||
|
try:
|
||||||
|
port_name.close()
|
||||||
|
except serial.SerialException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def query(port_name, cmd, verbose=True):
|
||||||
|
'''General command to send a query to the EiBotBoard'''
|
||||||
|
if port_name is not None and cmd is not None:
|
||||||
|
response = ''
|
||||||
|
try:
|
||||||
|
port_name.write(cmd.encode('ascii'))
|
||||||
|
response = port_name.readline().decode('ascii')
|
||||||
|
n_retry_count = 0
|
||||||
|
while len(response) == 0 and n_retry_count < 100:
|
||||||
|
# get new response to replace null response if necessary
|
||||||
|
response = port_name.readline()
|
||||||
|
n_retry_count += 1
|
||||||
|
if cmd.split(",")[0].strip().lower() not in ["a", "i", "mr", "pi", "qm", "qg", "v"]:
|
||||||
|
# Most queries return an "OK" after the data requested.
|
||||||
|
# We skip this for those few queries that do not return an extra line.
|
||||||
|
unused_response = port_name.readline() # read in extra blank/OK line
|
||||||
|
n_retry_count = 0
|
||||||
|
while len(unused_response) == 0 and n_retry_count < 100:
|
||||||
|
# get new response to replace null response if necessary
|
||||||
|
unused_response = port_name.readline()
|
||||||
|
n_retry_count += 1
|
||||||
|
except (serial.SerialException, IOError, RuntimeError, OSError) as err:
|
||||||
|
if verbose:
|
||||||
|
logger.error("Error reading serial data")
|
||||||
|
else:
|
||||||
|
logger.info("Error reading serial data")
|
||||||
|
logger.info("Error context:", exc_info=err)
|
||||||
|
|
||||||
|
if 'Err:' in response:
|
||||||
|
error_msg = '\n'.join(('Unexpected response from EBB.',
|
||||||
|
' Command: {0}'.format(cmd.strip()),
|
||||||
|
' Response: {0}'.format(response.strip())))
|
||||||
|
if verbose:
|
||||||
|
logger.error(error_msg)
|
||||||
|
else:
|
||||||
|
logger.info(error_msg)
|
||||||
|
return response
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def command(port_name, cmd, verbose=True):
|
||||||
|
'''General command to send a command to the EiBotBoard'''
|
||||||
|
if port_name is not None and cmd is not None:
|
||||||
|
try:
|
||||||
|
port_name.write(cmd.encode('ascii'))
|
||||||
|
response = port_name.readline().decode('ascii')
|
||||||
|
n_retry_count = 0
|
||||||
|
while len(response) == 0 and n_retry_count < 100:
|
||||||
|
# get new response to replace null response if necessary
|
||||||
|
response = port_name.readline().decode('ascii')
|
||||||
|
n_retry_count += 1
|
||||||
|
if response.strip().startswith("OK"):
|
||||||
|
# Debug option: indicate which command:
|
||||||
|
# inkex.errormsg( 'OK after command: ' + cmd )
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if response:
|
||||||
|
error_msg = '\n'.join(('Unexpected response from EBB.',
|
||||||
|
' Command: {0}'.format(cmd.strip()),
|
||||||
|
' Response: {0}'.format(response.strip())))
|
||||||
|
else:
|
||||||
|
error_msg = 'EBB Serial Timeout after command: {0}'.format(cmd)
|
||||||
|
if verbose:
|
||||||
|
logger.error(error_msg)
|
||||||
|
else:
|
||||||
|
logger.info(error_msg)
|
||||||
|
except (serial.SerialException, IOError, RuntimeError, OSError) as err:
|
||||||
|
if cmd.strip().lower() not in ["rb"]: # Ignore error on reboot (RB) command
|
||||||
|
if verbose:
|
||||||
|
logger.error('Failed after command: {0}'.format(cmd))
|
||||||
|
else:
|
||||||
|
logger.info('Failed after command: {0}'.format(cmd))
|
||||||
|
logger.info("Error context:", exc_info=err)
|
||||||
|
|
||||||
|
|
||||||
|
def bootload(port_name):
|
||||||
|
'''Enter bootloader mode. Do not try to read back data.'''
|
||||||
|
if port_name is not None:
|
||||||
|
try:
|
||||||
|
port_name.write('BL\r'.encode('ascii'))
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def min_version(port_name, version_string):
|
||||||
|
'''
|
||||||
|
Query the EBB firmware version for the EBB located at port_name.
|
||||||
|
Return True if the EBB firmware version is at least version_string.
|
||||||
|
Return False if the EBB firmware version is below version_string.
|
||||||
|
Return None if we are unable to determine True or False.
|
||||||
|
'''
|
||||||
|
if port_name is not None:
|
||||||
|
ebb_version_string = queryVersion(port_name) # Full string, human readable
|
||||||
|
ebb_version_string = ebb_version_string.split("Firmware Version ", 1)
|
||||||
|
|
||||||
|
if len(ebb_version_string) > 1:
|
||||||
|
ebb_version_string = ebb_version_string[1]
|
||||||
|
else:
|
||||||
|
return None # We haven't received a reasonable version number response.
|
||||||
|
|
||||||
|
ebb_version_string = ebb_version_string.strip() # Stripped copy, for number comparisons
|
||||||
|
if parse(ebb_version_string) >= parse(version_string):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def queryVersion(port_name):
|
||||||
|
'''Query EBB Version String'''
|
||||||
|
return query(port_name, 'V\r', True)
|
||||||
1181
inkscape/linux/patch/eggbot.py
Executable file
1181
inkscape/linux/patch/eggbot.py
Executable file
File diff suppressed because it is too large
Load Diff
6
inkscape/linux/readme.md
Normal file
6
inkscape/linux/readme.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Inkscape Plugin
|
||||||
|
This directory contains the plugin that fits to the EggBot firmware.
|
||||||
|
Use the ebb_serial.py file to patch the plugin so that all /dev/ttyUSBx devices can be used.
|
||||||
|
Currently only the first found tty USB device will be used.
|
||||||
|
|
||||||
|
If plugin doesn't work because of missing python dependencies, just install them with apt or similar system tool.
|
||||||
@@ -30,6 +30,7 @@ SerialCommand::SerialCommand()
|
|||||||
: commandList(NULL),
|
: commandList(NULL),
|
||||||
commandCount(0),
|
commandCount(0),
|
||||||
defaultHandler(NULL),
|
defaultHandler(NULL),
|
||||||
|
lineHandler(NULL),
|
||||||
term('\r'), // default terminator for commands, newline character
|
term('\r'), // default terminator for commands, newline character
|
||||||
last(NULL)
|
last(NULL)
|
||||||
{
|
{
|
||||||
@@ -64,6 +65,10 @@ void SerialCommand::setDefaultHandler(void (*function)(const char *)) {
|
|||||||
defaultHandler = function;
|
defaultHandler = function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SerialCommand::setLineHandler(LineHandler function) {
|
||||||
|
lineHandler = function;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This checks the Serial stream for characters, and assembles them into a buffer.
|
* This checks the Serial stream for characters, and assembles them into a buffer.
|
||||||
@@ -71,8 +76,17 @@ void SerialCommand::setDefaultHandler(void (*function)(const char *)) {
|
|||||||
* buffer for a prefix command, and calls handlers setup by addCommand() member
|
* buffer for a prefix command, and calls handlers setup by addCommand() member
|
||||||
*/
|
*/
|
||||||
void SerialCommand::readSerial() {
|
void SerialCommand::readSerial() {
|
||||||
while (Serial.available() > 0) {
|
readSerial(Serial);
|
||||||
char inChar = Serial.read(); // Read single available character, there may be more waiting
|
}
|
||||||
|
|
||||||
|
void SerialCommand::readSerial(Stream &stream) {
|
||||||
|
while (stream.available() > 0) {
|
||||||
|
char inChar = stream.read(); // Read single available character, there may be more waiting
|
||||||
|
readChar(inChar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SerialCommand::readChar(char inChar) {
|
||||||
#ifdef SERIALCOMMAND_DEBUG
|
#ifdef SERIALCOMMAND_DEBUG
|
||||||
Serial.print(inChar); // Echo back to serial stream
|
Serial.print(inChar); // Echo back to serial stream
|
||||||
#endif
|
#endif
|
||||||
@@ -83,6 +97,10 @@ void SerialCommand::readSerial() {
|
|||||||
Serial.println(buffer);
|
Serial.println(buffer);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if ((lineHandler != NULL) && (bufPos > 0)) {
|
||||||
|
(*lineHandler)(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
|
char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
|
||||||
if (command != NULL) {
|
if (command != NULL) {
|
||||||
boolean matched = false;
|
boolean matched = false;
|
||||||
@@ -113,8 +131,7 @@ void SerialCommand::readSerial() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
}
|
} else if (isprint(inChar)) { // Only printable characters into the buffer
|
||||||
else if (isprint(inChar)) { // Only printable characters into the buffer
|
|
||||||
if (bufPos < SERIALCOMMAND_BUFFER) {
|
if (bufPos < SERIALCOMMAND_BUFFER) {
|
||||||
buffer[bufPos++] = inChar; // Put character into buffer
|
buffer[bufPos++] = inChar; // Put character into buffer
|
||||||
buffer[bufPos] = '\0'; // Null terminate
|
buffer[bufPos] = '\0'; // Null terminate
|
||||||
@@ -125,7 +142,6 @@ void SerialCommand::readSerial() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Clear the input buffer.
|
* Clear the input buffer.
|
||||||
@@ -44,11 +44,16 @@
|
|||||||
|
|
||||||
class SerialCommand {
|
class SerialCommand {
|
||||||
public:
|
public:
|
||||||
|
typedef void (*LineHandler)(const char *);
|
||||||
|
|
||||||
SerialCommand(); // Constructor
|
SerialCommand(); // Constructor
|
||||||
void addCommand(const char *command, void(*function)()); // Add a command to the processing dictionary.
|
void addCommand(const char *command, void(*function)()); // Add a command to the processing dictionary.
|
||||||
void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received.
|
void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received.
|
||||||
|
void setLineHandler(LineHandler function); // Optional handler called with each completed input line.
|
||||||
|
|
||||||
void readSerial(); // Main entry point.
|
void readSerial(); // Main entry point.
|
||||||
|
void readSerial(Stream &stream); // Reads commands from any Stream-compatible transport.
|
||||||
|
void readChar(char inChar); // Feeds one incoming character into the parser.
|
||||||
void clearBuffer(); // Clears the input buffer.
|
void clearBuffer(); // Clears the input buffer.
|
||||||
char *next(); // Returns pointer to next token found in command buffer (for getting arguments to commands).
|
char *next(); // Returns pointer to next token found in command buffer (for getting arguments to commands).
|
||||||
|
|
||||||
@@ -63,6 +68,7 @@ class SerialCommand {
|
|||||||
|
|
||||||
// Pointer to the default handler function
|
// Pointer to the default handler function
|
||||||
void (*defaultHandler)(const char *);
|
void (*defaultHandler)(const char *);
|
||||||
|
LineHandler lineHandler;
|
||||||
|
|
||||||
char delim[2]; // null-terminated list of character to be used as delimeters for tokenizing (default " ")
|
char delim[2]; // null-terminated list of character to be used as delimeters for tokenizing (default " ")
|
||||||
char term; // Character that signals end of command (default '\n')
|
char term; // Character that signals end of command (default '\n')
|
||||||
41
platformio.ini
Normal file
41
platformio.ini
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
; 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:uno]
|
||||||
|
platform = platformio/espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
upload_speed = 576000
|
||||||
|
upload_port = /dev/ttyUSB*
|
||||||
|
extra_scripts = post:scripts/package_firmware.py
|
||||||
|
lib_deps =
|
||||||
|
arminjo/ServoEasing
|
||||||
|
madhephaestus/ESP32Servo@^3.0.6
|
||||||
|
bblanchon/ArduinoJson@^6.21.5
|
||||||
|
gin66/FastAccelStepper@^0.33.13
|
||||||
|
h2zero/NimBLE-Arduino@^2.3.6
|
||||||
|
links2004/WebSockets@^2.6.1
|
||||||
|
|
||||||
|
[env:uno_macos]
|
||||||
|
platform = platformio/espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_port = /dev/cu.usb*
|
||||||
|
upload_port = /dev/cu.usb*
|
||||||
|
extra_scripts = post:scripts/package_firmware.py
|
||||||
|
lib_deps =
|
||||||
|
arminjo/ServoEasing
|
||||||
|
madhephaestus/ESP32Servo@^3.0.6
|
||||||
|
bblanchon/ArduinoJson@^6.21.5
|
||||||
|
gin66/FastAccelStepper@^0.33.13
|
||||||
|
h2zero/NimBLE-Arduino@^2.3.6
|
||||||
|
links2004/WebSockets@^2.6.1
|
||||||
12
scripts/check_config_web_stack_usage.sh
Executable file
12
scripts/check_config_web_stack_usage.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
if rg -n '^[[:space:]]+StaticJsonDocument<kConfigJsonCapacity>[[:space:]]+[A-Za-z_][A-Za-z0-9_]*;' src/Config_Web.cpp; then
|
||||||
|
echo "Config_Web.cpp still allocates kConfigJsonCapacity-sized StaticJsonDocument on the stack" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Config_Web.cpp avoids stack-local kConfigJsonCapacity StaticJsonDocument allocations"
|
||||||
101
scripts/package_firmware.py
Normal file
101
scripts/package_firmware.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BUILD_ARTIFACTS = (
|
||||||
|
"bootloader.bin",
|
||||||
|
"partitions.bin",
|
||||||
|
"firmware.bin",
|
||||||
|
"firmware.elf",
|
||||||
|
"firmware.map",
|
||||||
|
)
|
||||||
|
|
||||||
|
MANIFEST_PARTS = (
|
||||||
|
{"path": "bootloader.bin", "offset": 4096},
|
||||||
|
{"path": "partitions.bin", "offset": 32768},
|
||||||
|
{"path": "boot_app0.bin", "offset": 57344},
|
||||||
|
{"path": "firmware.bin", "offset": 65536},
|
||||||
|
)
|
||||||
|
|
||||||
|
VERSION_PATTERN = re.compile(r'Eggduino-Firmware V([^"\\]+)')
|
||||||
|
|
||||||
|
|
||||||
|
def extract_firmware_version(header_text: str) -> str:
|
||||||
|
match = VERSION_PATTERN.search(header_text)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("Could not extract firmware version from include/EggDuino.h")
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def build_manifest(version: str) -> dict:
|
||||||
|
return {
|
||||||
|
"name": "EggDuino ESP32 Firmware",
|
||||||
|
"version": version,
|
||||||
|
"new_install_prompt_erase": True,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"chipFamily": "ESP32",
|
||||||
|
"parts": list(MANIFEST_PARTS),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def package_bundle(build_dir: Path, boot_app0_path: Path, header_path: Path, output_dir: Path) -> None:
|
||||||
|
build_dir = Path(build_dir)
|
||||||
|
boot_app0_path = Path(boot_app0_path)
|
||||||
|
header_path = Path(header_path)
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
|
||||||
|
if output_dir.exists():
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for artifact_name in BUILD_ARTIFACTS:
|
||||||
|
artifact_path = build_dir / artifact_name
|
||||||
|
if not artifact_path.is_file():
|
||||||
|
raise FileNotFoundError(f"Missing build artifact: {artifact_path}")
|
||||||
|
shutil.copy2(artifact_path, output_dir / artifact_name)
|
||||||
|
|
||||||
|
if not boot_app0_path.is_file():
|
||||||
|
raise FileNotFoundError(f"Missing boot_app0.bin: {boot_app0_path}")
|
||||||
|
shutil.copy2(boot_app0_path, output_dir / "boot_app0.bin")
|
||||||
|
|
||||||
|
version = extract_firmware_version(header_path.read_text(encoding="utf-8"))
|
||||||
|
manifest_path = output_dir / "manifest.json"
|
||||||
|
manifest_path.write_text(
|
||||||
|
json.dumps(build_manifest(version), indent=4) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_boot_app0_path(env) -> Path:
|
||||||
|
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif32")
|
||||||
|
if not framework_dir:
|
||||||
|
raise FileNotFoundError("PlatformIO framework-arduinoespressif32 package is not installed")
|
||||||
|
|
||||||
|
boot_app0_path = Path(framework_dir) / "tools" / "partitions" / "boot_app0.bin"
|
||||||
|
if not boot_app0_path.is_file():
|
||||||
|
raise FileNotFoundError(f"Could not find boot_app0.bin in {boot_app0_path.parent}")
|
||||||
|
return boot_app0_path
|
||||||
|
|
||||||
|
|
||||||
|
def _package_platformio_bundle(target, source, env) -> None:
|
||||||
|
del target, source
|
||||||
|
project_dir = Path(env.subst("$PROJECT_DIR"))
|
||||||
|
package_bundle(
|
||||||
|
build_dir=Path(env.subst("$BUILD_DIR")),
|
||||||
|
boot_app0_path=_get_boot_app0_path(env),
|
||||||
|
header_path=project_dir / "include" / "EggDuino.h",
|
||||||
|
output_dir=project_dir / "firmware",
|
||||||
|
)
|
||||||
|
print(f"Packaged firmware bundle to {project_dir / 'firmware'}")
|
||||||
|
|
||||||
|
|
||||||
|
if "Import" in globals():
|
||||||
|
Import("env")
|
||||||
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", _package_platformio_bundle)
|
||||||
231
src/BLE_Interface.cpp
Normal file
231
src/BLE_Interface.cpp
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#include "EggDuino.h"
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr char kBleDeviceNamePrefix[] = "EggBot_";
|
||||||
|
constexpr char kBleServiceUuid[] = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
constexpr char kBleRxCharUuid[] = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
constexpr char kBleTxCharUuid[] = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
|
||||||
|
constexpr size_t kBleRxQueueSize = 512;
|
||||||
|
constexpr size_t kBleNotifyChunkSize = 20;
|
||||||
|
|
||||||
|
NimBLEServer *g_pBleServer = NULL;
|
||||||
|
NimBLECharacteristic *g_pBleTxCharacteristic = NULL;
|
||||||
|
|
||||||
|
uint8_t g_bleRxQueue[kBleRxQueueSize];
|
||||||
|
size_t g_bleRxHead = 0;
|
||||||
|
size_t g_bleRxTail = 0;
|
||||||
|
bool g_bleRxQueueOverflow = false;
|
||||||
|
bool g_bleClientConnected = false;
|
||||||
|
|
||||||
|
portMUX_TYPE g_bleQueueMux = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
|
||||||
|
void logBleDiag(const String &message)
|
||||||
|
{
|
||||||
|
Log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool queueBleByte(uint8_t value)
|
||||||
|
{
|
||||||
|
bool queued = false;
|
||||||
|
portENTER_CRITICAL(&g_bleQueueMux);
|
||||||
|
const size_t nextHead = (g_bleRxHead + 1) % kBleRxQueueSize;
|
||||||
|
if (nextHead == g_bleRxTail)
|
||||||
|
{
|
||||||
|
g_bleRxQueueOverflow = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_bleRxQueue[g_bleRxHead] = value;
|
||||||
|
g_bleRxHead = nextHead;
|
||||||
|
queued = true;
|
||||||
|
}
|
||||||
|
portEXIT_CRITICAL(&g_bleQueueMux);
|
||||||
|
return queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dequeueBleByte(uint8_t *value)
|
||||||
|
{
|
||||||
|
bool hasData = false;
|
||||||
|
portENTER_CRITICAL(&g_bleQueueMux);
|
||||||
|
if (g_bleRxHead != g_bleRxTail)
|
||||||
|
{
|
||||||
|
*value = g_bleRxQueue[g_bleRxTail];
|
||||||
|
g_bleRxTail = (g_bleRxTail + 1) % kBleRxQueueSize;
|
||||||
|
hasData = true;
|
||||||
|
}
|
||||||
|
portEXIT_CRITICAL(&g_bleQueueMux);
|
||||||
|
return hasData;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EggDuinoBleServerCallbacks : public NimBLEServerCallbacks
|
||||||
|
{
|
||||||
|
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
|
||||||
|
{
|
||||||
|
(void)pServer;
|
||||||
|
(void)connInfo;
|
||||||
|
g_bleClientConnected = true;
|
||||||
|
Log("BLE client connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
|
||||||
|
{
|
||||||
|
(void)connInfo;
|
||||||
|
(void)reason;
|
||||||
|
g_bleClientConnected = false;
|
||||||
|
const bool restartedAdvertising = pServer->startAdvertising();
|
||||||
|
logBleDiag(String("BLE client disconnected; advertising restart: ") + (restartedAdvertising ? "ok" : "failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class EggDuinoBleRxCallbacks : public NimBLECharacteristicCallbacks
|
||||||
|
{
|
||||||
|
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||||
|
{
|
||||||
|
(void)connInfo;
|
||||||
|
const std::string value = pCharacteristic->getValue();
|
||||||
|
for (size_t i = 0; i < value.size(); ++i)
|
||||||
|
{
|
||||||
|
queueBleByte(static_cast<uint8_t>(value[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void startBleInterface()
|
||||||
|
{
|
||||||
|
char bleDeviceName[32] = {0};
|
||||||
|
buildDeviceName(bleDeviceName, sizeof(bleDeviceName));
|
||||||
|
if (bleDeviceName[0] == '\0')
|
||||||
|
{
|
||||||
|
snprintf(bleDeviceName, sizeof(bleDeviceName), "%sUNKNOWN", kBleDeviceNamePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
logBleDiag("BLE init begin");
|
||||||
|
logBleDiag(String("BLE device name: ") + bleDeviceName);
|
||||||
|
logBleDiag(String("BLE service UUID: ") + kBleServiceUuid);
|
||||||
|
logBleDiag(String("BLE RX UUID: ") + kBleRxCharUuid);
|
||||||
|
logBleDiag(String("BLE TX UUID: ") + kBleTxCharUuid);
|
||||||
|
|
||||||
|
NimBLEDevice::init(bleDeviceName);
|
||||||
|
const bool blePowerSet = NimBLEDevice::setPower(ESP_PWR_LVL_P6);
|
||||||
|
const std::string bleAddress = NimBLEDevice::getAddress().toString();
|
||||||
|
logBleDiag(String("BLE radio address: ") + bleAddress.c_str());
|
||||||
|
logBleDiag(String("BLE TX power set: ") + (blePowerSet ? "ok" : "failed"));
|
||||||
|
|
||||||
|
g_pBleServer = NimBLEDevice::createServer();
|
||||||
|
if (g_pBleServer == NULL)
|
||||||
|
{
|
||||||
|
logBleDiag("BLE init failed: createServer returned null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_pBleServer->setCallbacks(new EggDuinoBleServerCallbacks());
|
||||||
|
logBleDiag("BLE server created");
|
||||||
|
|
||||||
|
NimBLEService *pService = g_pBleServer->createService(kBleServiceUuid);
|
||||||
|
if (pService == NULL)
|
||||||
|
{
|
||||||
|
logBleDiag("BLE init failed: createService returned null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_pBleTxCharacteristic = pService->createCharacteristic(
|
||||||
|
kBleTxCharUuid,
|
||||||
|
NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ);
|
||||||
|
if (g_pBleTxCharacteristic == NULL)
|
||||||
|
{
|
||||||
|
logBleDiag("BLE init failed: TX characteristic creation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NimBLECharacteristic *pRxCharacteristic = pService->createCharacteristic(
|
||||||
|
kBleRxCharUuid,
|
||||||
|
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR);
|
||||||
|
if (pRxCharacteristic == NULL)
|
||||||
|
{
|
||||||
|
logBleDiag("BLE init failed: RX characteristic creation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pRxCharacteristic->setCallbacks(new EggDuinoBleRxCallbacks());
|
||||||
|
logBleDiag("BLE characteristics created");
|
||||||
|
|
||||||
|
const bool serviceStarted = pService->start();
|
||||||
|
logBleDiag(String("BLE service start: ") + (serviceStarted ? "ok" : "failed"));
|
||||||
|
if (!serviceStarted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||||
|
if (pAdvertising == NULL)
|
||||||
|
{
|
||||||
|
logBleDiag("BLE init failed: getAdvertising returned null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pAdvertising->enableScanResponse(true);
|
||||||
|
const bool localNameSet = pAdvertising->setName(bleDeviceName);
|
||||||
|
const bool serviceUuidAdded = pAdvertising->addServiceUUID(kBleServiceUuid);
|
||||||
|
const bool advertisingStarted = pAdvertising->start();
|
||||||
|
logBleDiag(String("BLE advertising set local name: ") + (localNameSet ? "ok" : "failed"));
|
||||||
|
logBleDiag(String("BLE advertising add service UUID: ") + (serviceUuidAdded ? "ok" : "failed"));
|
||||||
|
logBleDiag(String("BLE advertising start: ") + (advertisingStarted ? "ok" : "failed"));
|
||||||
|
if (serviceUuidAdded && advertisingStarted)
|
||||||
|
{
|
||||||
|
logBleDiag("BLE service started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleBleInterface()
|
||||||
|
{
|
||||||
|
if (g_bleRxQueueOverflow)
|
||||||
|
{
|
||||||
|
g_bleRxQueueOverflow = false;
|
||||||
|
Log("BLE RX queue overflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t value = 0;
|
||||||
|
while (dequeueBleByte(&value))
|
||||||
|
{
|
||||||
|
setActiveProtocolContext(&g_BLECmd, PROTOCOL_TRANSPORT_BLE);
|
||||||
|
g_BLECmd.readChar(static_cast<char>(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bleProtocolWrite(const char *message)
|
||||||
|
{
|
||||||
|
if ((message == NULL) || !g_bleClientConnected || (g_pBleTxCharacteristic == NULL))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *payload = reinterpret_cast<const uint8_t *>(message);
|
||||||
|
size_t remaining = strlen(message);
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
size_t chunkLen = remaining;
|
||||||
|
if (chunkLen > kBleNotifyChunkSize)
|
||||||
|
{
|
||||||
|
chunkLen = kBleNotifyChunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_pBleTxCharacteristic->setValue(payload, chunkLen);
|
||||||
|
g_pBleTxCharacteristic->notify();
|
||||||
|
|
||||||
|
payload += chunkLen;
|
||||||
|
remaining -= chunkLen;
|
||||||
|
delay(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
655
src/Config_Web.cpp
Normal file
655
src/Config_Web.cpp
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
#include "EggDuino.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <DNSServer.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const char *kConfigPath = "/config.json";
|
||||||
|
const size_t kConfigJsonCapacity = 4096;
|
||||||
|
const byte kDnsPort = 53;
|
||||||
|
const unsigned long kWifiReconnectIntervalMs = 10000;
|
||||||
|
using ConfigJsonDocument = StaticJsonDocument<kConfigJsonCapacity>;
|
||||||
|
|
||||||
|
WebServer server(80);
|
||||||
|
DNSServer dnsServer;
|
||||||
|
bool configStoreReady = false;
|
||||||
|
bool apModeActive = false;
|
||||||
|
bool staReconnectEnabled = false;
|
||||||
|
bool staConnectionKnown = false;
|
||||||
|
unsigned long lastStaReconnectAttemptMs = 0;
|
||||||
|
ConfigJsonDocument g_configJsonScratch;
|
||||||
|
|
||||||
|
ConfigJsonDocument &configJsonScratch()
|
||||||
|
{
|
||||||
|
// Keep the large config JSON buffer off ESP32 Arduino's 8 KB loopTask stack.
|
||||||
|
g_configJsonScratch.clear();
|
||||||
|
return g_configJsonScratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void redirectToRoot()
|
||||||
|
{
|
||||||
|
server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString() + "/", true);
|
||||||
|
server.send(302, "text/plain", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIntType(const ConfigParameter ¶m)
|
||||||
|
{
|
||||||
|
return strcmp(param.type, "int") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int *asIntPtr(ConfigParameter ¶m)
|
||||||
|
{
|
||||||
|
return static_cast<int *>(param.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
String *asStringPtr(ConfigParameter ¶m)
|
||||||
|
{
|
||||||
|
return static_cast<String *>(param.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigParameter *findParameter(const String &key)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < configParameterCount; ++i)
|
||||||
|
{
|
||||||
|
if (key.equals(configParameters[i].key))
|
||||||
|
{
|
||||||
|
return &configParameters[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyDefaults()
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < configParameterCount; ++i)
|
||||||
|
{
|
||||||
|
if (isIntType(configParameters[i]))
|
||||||
|
{
|
||||||
|
*asIntPtr(configParameters[i]) = configParameters[i].defaultValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const char *fallback = configParameters[i].defaultText == nullptr ? "" : configParameters[i].defaultText;
|
||||||
|
*asStringPtr(configParameters[i]) = String(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleRoot()
|
||||||
|
{
|
||||||
|
static const char kPage[] PROGMEM = R"HTML(
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>EggDuino Konfiguration</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: "Segoe UI", sans-serif; margin: 20px; background: #f3f6fb; color: #1a1a1a; }
|
||||||
|
main { max-width: 760px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
||||||
|
h1 { margin-top: 0; font-size: 1.35rem; }
|
||||||
|
label { display: block; margin: 14px 0 6px; font-weight: 600; }
|
||||||
|
input[type='number'], input[type='text'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #c7d2e5; border-radius: 8px; box-sizing: border-box; }
|
||||||
|
button { margin-top: 18px; border: 0; background: #0b5ed7; color: white; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
|
||||||
|
#status { margin-top: 12px; min-height: 1.2em; }
|
||||||
|
#log { margin-top: 20px; border: 1px solid #d6dfef; border-radius: 8px; background: #0f172a; color: #d2e3ff; padding: 10px; height: 220px; overflow-y: auto; white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.9rem; }
|
||||||
|
#logTitle { margin-top: 24px; margin-bottom: 8px; font-weight: 700; }
|
||||||
|
#downloadPrintLog { display: inline-block; margin-top: 8px; color: #0b5ed7; text-decoration: none; font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>EggDuino Parameter</h1>
|
||||||
|
<form id="cfgForm"></form>
|
||||||
|
<button id="saveBtn" type="button">Speichern</button>
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="logTitle">Logs</div>
|
||||||
|
<a id="downloadPrintLog" href="/api/print-log.txt" download="incoming-print-log.txt">Incoming print log herunterladen</a>
|
||||||
|
<div id="log"></div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
let lastSeq = 0;
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
const resp = await fetch('/api/config', { cache: 'no-store' });
|
||||||
|
if (!resp.ok) throw new Error('Konfiguration konnte nicht geladen werden');
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderForm(config) {
|
||||||
|
const form = document.getElementById('cfgForm');
|
||||||
|
form.innerHTML = '';
|
||||||
|
(config.parameters || []).forEach(p => {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = p.description || p.key;
|
||||||
|
const input = document.createElement('input');
|
||||||
|
const paramType = p.type || 'int';
|
||||||
|
input.type = paramType === 'int' ? 'number' : paramType;
|
||||||
|
if (paramType === 'password') {
|
||||||
|
input.value = '';
|
||||||
|
if (p.hasValue) {
|
||||||
|
input.placeholder = '(gespeichert)';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.value = p.value ?? '';
|
||||||
|
}
|
||||||
|
input.dataset.key = p.key;
|
||||||
|
input.dataset.ptype = paramType;
|
||||||
|
input.dataset.secret = p.secret ? '1' : '0';
|
||||||
|
input.dataset.touched = '0';
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
input.dataset.touched = '1';
|
||||||
|
});
|
||||||
|
form.appendChild(label);
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const inputs = [...document.querySelectorAll('input[data-key]')];
|
||||||
|
const payload = {
|
||||||
|
parameters: inputs
|
||||||
|
.map(i => {
|
||||||
|
const type = i.dataset.ptype || 'int';
|
||||||
|
const secret = i.dataset.secret === '1';
|
||||||
|
if (type === 'int') {
|
||||||
|
return { key: i.dataset.key, value: Number(i.value) };
|
||||||
|
}
|
||||||
|
if (secret && i.dataset.touched !== '1') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { key: i.dataset.key, value: i.value };
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
};
|
||||||
|
const resp = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const text = await resp.text();
|
||||||
|
throw new Error(text || 'Speichern fehlgeschlagen');
|
||||||
|
}
|
||||||
|
const saved = await resp.json();
|
||||||
|
renderForm(saved);
|
||||||
|
status.textContent = 'Gespeichert';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollLogs() {
|
||||||
|
const box = document.getElementById('log');
|
||||||
|
const resp = await fetch('/api/logs?since=' + lastSeq);
|
||||||
|
if (!resp.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = await resp.json();
|
||||||
|
(payload.logs || []).forEach(entry => {
|
||||||
|
box.textContent += entry.text + '\n';
|
||||||
|
});
|
||||||
|
if (typeof payload.lastSeq === 'number') {
|
||||||
|
lastSeq = payload.lastSeq;
|
||||||
|
}
|
||||||
|
box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function init() {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
try {
|
||||||
|
const cfg = await loadConfig();
|
||||||
|
renderForm(cfg);
|
||||||
|
status.textContent = 'Bereit';
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = e.message;
|
||||||
|
}
|
||||||
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await saveConfig();
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pollLogs();
|
||||||
|
setInterval(pollLogs, 800);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)HTML";
|
||||||
|
|
||||||
|
server.send(200, "text/html", kPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleGetConfig()
|
||||||
|
{
|
||||||
|
if (!configStoreReady && !initConfigStore())
|
||||||
|
{
|
||||||
|
server.send(500, "text/plain", "Config storage not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||||
|
server.sendHeader("Pragma", "no-cache");
|
||||||
|
server.sendHeader("Expires", "0");
|
||||||
|
server.send(200, "application/json", buildConfigJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void handlePostConfig()
|
||||||
|
{
|
||||||
|
if (!configStoreReady && !initConfigStore())
|
||||||
|
{
|
||||||
|
server.send(500, "text/plain", "Config storage not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.hasArg("plain"))
|
||||||
|
{
|
||||||
|
server.send(400, "text/plain", "Missing JSON body");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String error;
|
||||||
|
if (!applyConfigJson(server.arg("plain"), error))
|
||||||
|
{
|
||||||
|
Log(String("Config JSON fehlerhaft: ") + error);
|
||||||
|
server.send(400, "text/plain", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saveConfigToFile())
|
||||||
|
{
|
||||||
|
Log("Config konnte nicht gespeichert werden");
|
||||||
|
server.send(500, "text/plain", "Could not save config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.send(200, "application/json", buildConfigJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleGetLogs()
|
||||||
|
{
|
||||||
|
uint32_t since = 0;
|
||||||
|
if (server.hasArg("since"))
|
||||||
|
{
|
||||||
|
since = static_cast<uint32_t>(server.arg("since").toInt());
|
||||||
|
}
|
||||||
|
server.send(200, "application/json", buildLogsJson(since));
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDownloadPrintLog()
|
||||||
|
{
|
||||||
|
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||||
|
server.sendHeader("Pragma", "no-cache");
|
||||||
|
server.sendHeader("Expires", "0");
|
||||||
|
server.sendHeader("Content-Disposition", "attachment; filename=\"incoming-print-log.txt\"");
|
||||||
|
server.send(200, "text/plain; charset=utf-8", buildIncomingPrintLogText());
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleNotFound()
|
||||||
|
{
|
||||||
|
if (apModeActive)
|
||||||
|
{
|
||||||
|
redirectToRoot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (server.uri().startsWith("/api/"))
|
||||||
|
{
|
||||||
|
server.send(404, "text/plain", "API endpoint not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleWifiReconnect()
|
||||||
|
{
|
||||||
|
if (!staReconnectEnabled || apModeActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wl_status_t wifiStatus = WiFi.status();
|
||||||
|
if (wifiStatus == WL_CONNECTED)
|
||||||
|
{
|
||||||
|
if (!staConnectionKnown)
|
||||||
|
{
|
||||||
|
staConnectionKnown = true;
|
||||||
|
Log(String("WLAN verbunden: ") + WiFi.localIP().toString());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staConnectionKnown)
|
||||||
|
{
|
||||||
|
staConnectionKnown = false;
|
||||||
|
Log("WLAN Verbindung verloren");
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned long now = millis();
|
||||||
|
if (now - lastStaReconnectAttemptMs < kWifiReconnectIntervalMs)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastStaReconnectAttemptMs = now;
|
||||||
|
if (!WiFi.reconnect())
|
||||||
|
{
|
||||||
|
WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());
|
||||||
|
}
|
||||||
|
Log("WLAN Reconnect versucht");
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ConfigParameter configParameters[] = {
|
||||||
|
{"int", "penUpPos", &g_iPenUpPos, "Pen Up Position", 40, "", false},
|
||||||
|
{"int", "penDownPos", &g_iPenDownPos, "Pen Down Position", 10, "", false},
|
||||||
|
{"int", "rotStepPin", &g_iRotStepPin, "Rotational Stepper Step Pin", kDefaultRotStepPin, "", false},
|
||||||
|
{"int", "rotDirPin", &g_iRotDirPin, "Rotational Stepper Direction Pin", kDefaultRotDirPin, "", false},
|
||||||
|
{"int", "rotEnablePin", &g_iRotEnablePin, "Rotational Stepper Enable Pin", kDefaultRotEnablePin, "", false},
|
||||||
|
{"int", "penStepPin", &g_iPenStepPin, "Pen Stepper Step Pin", kDefaultPenStepPin, "", false},
|
||||||
|
{"int", "penDirPin", &g_iPenDirPin, "Pen Stepper Direction Pin", kDefaultPenDirPin, "", false},
|
||||||
|
{"int", "penEnablePin", &g_iPenEnablePin, "Pen Stepper Enable Pin", kDefaultPenEnablePin, "", false},
|
||||||
|
{"int", "rotMicrostep", &g_iRotMicrostep, "Rotational Stepper Microsteps", kDefaultRotMicrostep, "", false},
|
||||||
|
{"int", "penMicrostep", &g_iPenMicrostep, "Pen Stepper Microsteps", kDefaultPenMicrostep, "", false},
|
||||||
|
{"int", "iMaxAcclSpeed", &g_iMaxAcclSpeed, "max. Stepper acceleration speed", 10000, "", false},
|
||||||
|
{"int", "servoPin", &g_iServoPin, "Servo Pin", kDefaultServoPin, "", false},
|
||||||
|
{"text", "Name", &g_sHostname, "Name", 0, "EggDuino", false},
|
||||||
|
{"text", "wifiSsid", &g_sWifiSsid, "WLAN SSID", 0, "", false},
|
||||||
|
{"password", "wifiPassword", &g_sWifiPassword, "WLAN Passwort", 0, "", true},
|
||||||
|
};
|
||||||
|
|
||||||
|
const size_t configParameterCount = sizeof(configParameters) / sizeof(configParameters[0]);
|
||||||
|
|
||||||
|
bool initConfigStore()
|
||||||
|
{
|
||||||
|
if (!SPIFFS.begin(true))
|
||||||
|
{
|
||||||
|
configStoreReady = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
configStoreReady = loadConfigFromFile();
|
||||||
|
return configStoreReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadConfigFromFile()
|
||||||
|
{
|
||||||
|
applyDefaults();
|
||||||
|
|
||||||
|
File file = SPIFFS.open(kConfigPath, "r");
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
Log("config.json fehlt, defaults werden gespeichert");
|
||||||
|
return saveConfigToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigJsonDocument &doc = configJsonScratch();
|
||||||
|
DeserializationError err = deserializeJson(doc, file);
|
||||||
|
file.close();
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
Log(String("config.json ist ungueltig (") + err.c_str() + "), defaults werden gespeichert");
|
||||||
|
return saveConfigToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray params = doc["parameters"].as<JsonArray>();
|
||||||
|
for (JsonObject item : params)
|
||||||
|
{
|
||||||
|
const char *key = item["key"];
|
||||||
|
if (key == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ConfigParameter *param = findParameter(String(key));
|
||||||
|
if (param == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.containsKey("value"))
|
||||||
|
{
|
||||||
|
if (isIntType(*param))
|
||||||
|
{
|
||||||
|
*asIntPtr(*param) = item["value"].as<int>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
*asStringPtr(*param) = item["value"].as<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.containsKey("description"))
|
||||||
|
{
|
||||||
|
param->description = item["description"].as<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateStepCorrectionFactors();
|
||||||
|
Log(String("Config geladen: penUpPos=") + g_iPenUpPos + ", penDownPos=" + g_iPenDownPos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveConfigToFile()
|
||||||
|
{
|
||||||
|
File file = SPIFFS.open(kConfigPath, "w");
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
Log("SPIFFS open write failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigJsonDocument &doc = configJsonScratch();
|
||||||
|
JsonArray params = doc.createNestedArray("parameters");
|
||||||
|
for (size_t i = 0; i < configParameterCount; ++i)
|
||||||
|
{
|
||||||
|
JsonObject item = params.createNestedObject();
|
||||||
|
item["key"] = configParameters[i].key;
|
||||||
|
item["type"] = configParameters[i].type;
|
||||||
|
item["secret"] = configParameters[i].secret;
|
||||||
|
if (isIntType(configParameters[i]))
|
||||||
|
{
|
||||||
|
item["value"] = *asIntPtr(configParameters[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item["value"] = *asStringPtr(configParameters[i]);
|
||||||
|
}
|
||||||
|
item["description"] = configParameters[i].description;
|
||||||
|
}
|
||||||
|
if (doc.overflowed())
|
||||||
|
{
|
||||||
|
Log("Config JSON Dokument zu klein beim Speichern");
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = serializeJsonPretty(doc, file) > 0;
|
||||||
|
file.flush();
|
||||||
|
file.close();
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
Log("serializeJsonPretty failed");
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildConfigJson()
|
||||||
|
{
|
||||||
|
ConfigJsonDocument &doc = configJsonScratch();
|
||||||
|
JsonArray params = doc.createNestedArray("parameters");
|
||||||
|
for (size_t i = 0; i < configParameterCount; ++i)
|
||||||
|
{
|
||||||
|
JsonObject item = params.createNestedObject();
|
||||||
|
item["key"] = configParameters[i].key;
|
||||||
|
item["type"] = configParameters[i].type;
|
||||||
|
item["secret"] = configParameters[i].secret;
|
||||||
|
if (isIntType(configParameters[i]))
|
||||||
|
{
|
||||||
|
item["value"] = *asIntPtr(configParameters[i]);
|
||||||
|
}
|
||||||
|
else if (configParameters[i].secret)
|
||||||
|
{
|
||||||
|
item["value"] = "";
|
||||||
|
item["hasValue"] = !asStringPtr(configParameters[i])->isEmpty();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item["value"] = *asStringPtr(configParameters[i]);
|
||||||
|
}
|
||||||
|
item["description"] = configParameters[i].description;
|
||||||
|
}
|
||||||
|
if (doc.overflowed())
|
||||||
|
{
|
||||||
|
Log("Config JSON Dokument zu klein beim Lesen");
|
||||||
|
return String("{\"parameters\":[]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applyConfigJson(const String &payload, String &errorMessage)
|
||||||
|
{
|
||||||
|
ConfigJsonDocument &doc = configJsonScratch();
|
||||||
|
DeserializationError err = deserializeJson(doc, payload);
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
errorMessage = "Invalid JSON payload";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray params = doc["parameters"].as<JsonArray>();
|
||||||
|
if (params.isNull())
|
||||||
|
{
|
||||||
|
errorMessage = "JSON must contain 'parameters' array";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (JsonObject item : params)
|
||||||
|
{
|
||||||
|
const char *key = item["key"];
|
||||||
|
if (key == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigParameter *param = findParameter(String(key));
|
||||||
|
if (param == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.containsKey("value"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isIntType(*param))
|
||||||
|
{
|
||||||
|
*asIntPtr(*param) = item["value"].as<int>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
*asStringPtr(*param) = item["value"].as<String>();
|
||||||
|
}
|
||||||
|
if (item.containsKey("description"))
|
||||||
|
{
|
||||||
|
param->description = item["description"].as<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateStepCorrectionFactors();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void startWebInterface()
|
||||||
|
{
|
||||||
|
initConfigStore();
|
||||||
|
bool staConnected = false;
|
||||||
|
apModeActive = false;
|
||||||
|
staReconnectEnabled = !g_sWifiSsid.isEmpty();
|
||||||
|
staConnectionKnown = false;
|
||||||
|
lastStaReconnectAttemptMs = millis();
|
||||||
|
dnsServer.stop();
|
||||||
|
char deviceName[32] = {0};
|
||||||
|
buildDeviceName(deviceName, sizeof(deviceName));
|
||||||
|
String hostName = String(deviceName);
|
||||||
|
if (hostName.isEmpty())
|
||||||
|
{
|
||||||
|
hostName = "EggBot_UNKNOWN";
|
||||||
|
}
|
||||||
|
g_sHostname = hostName;
|
||||||
|
|
||||||
|
if (!g_sWifiSsid.isEmpty())
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.setAutoReconnect(true);
|
||||||
|
WiFi.setHostname(hostName.c_str());
|
||||||
|
WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());
|
||||||
|
|
||||||
|
const unsigned long connectStart = millis();
|
||||||
|
const unsigned long connectTimeoutMs = 20000;
|
||||||
|
Serial.println(String("Trying to connect to Wifi: ") + g_sWifiSsid.c_str() + g_sWifiPassword.c_str());
|
||||||
|
while (WiFi.status() != WL_CONNECTED && millis() - connectStart < connectTimeoutMs)
|
||||||
|
{
|
||||||
|
delay(250);
|
||||||
|
}
|
||||||
|
staConnected = (WiFi.status() == WL_CONNECTED);
|
||||||
|
staConnectionKnown = staConnected;
|
||||||
|
if (staConnected)
|
||||||
|
{
|
||||||
|
Serial.println(String("Connected: http://") + WiFi.localIP().toString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("WLAN Verbindung fehlgeschlagen");
|
||||||
|
}
|
||||||
|
|
||||||
|
server.on("/", HTTP_GET, handleRoot);
|
||||||
|
server.on("/api/config", HTTP_GET, handleGetConfig);
|
||||||
|
server.on("/api/config", HTTP_POST, handlePostConfig);
|
||||||
|
server.on("/api/logs", HTTP_GET, handleGetLogs);
|
||||||
|
server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
|
||||||
|
server.onNotFound(handleNotFound);
|
||||||
|
server.begin();
|
||||||
|
startWifiProtocolInterface();
|
||||||
|
}
|
||||||
|
if (!staConnected)
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
#ifdef ESP32
|
||||||
|
WiFi.softAPsetHostname(hostName.c_str());
|
||||||
|
#endif
|
||||||
|
if (WiFi.softAP(hostName.c_str()))
|
||||||
|
{
|
||||||
|
apModeActive = true;
|
||||||
|
dnsServer.start(kDnsPort, "*", WiFi.softAPIP());
|
||||||
|
Serial.println(String("AP aktiv: ") + hostName + " / http://" + WiFi.softAPIP().toString() + " (Name: " + hostName + ")");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("AP konnte nicht gestartet werden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.on("/", HTTP_GET, handleRoot);
|
||||||
|
server.on("/api/config", HTTP_GET, handleGetConfig);
|
||||||
|
server.on("/api/config", HTTP_POST, handlePostConfig);
|
||||||
|
server.on("/api/logs", HTTP_GET, handleGetLogs);
|
||||||
|
server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
|
||||||
|
server.on("/generate_204", HTTP_GET, redirectToRoot);
|
||||||
|
server.on("/gen_204", HTTP_GET, redirectToRoot);
|
||||||
|
server.on("/hotspot-detect.html", HTTP_GET, redirectToRoot);
|
||||||
|
server.on("/connecttest.txt", HTTP_GET, redirectToRoot);
|
||||||
|
server.on("/ncsi.txt", HTTP_GET, redirectToRoot);
|
||||||
|
server.on("/fwlink", HTTP_GET, redirectToRoot);
|
||||||
|
server.onNotFound(handleNotFound);
|
||||||
|
server.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleWebInterface()
|
||||||
|
{
|
||||||
|
handleWifiReconnect();
|
||||||
|
|
||||||
|
if (apModeActive)
|
||||||
|
{
|
||||||
|
dnsServer.processNextRequest();
|
||||||
|
}
|
||||||
|
server.handleClient();
|
||||||
|
}
|
||||||
371
src/Functions.cpp
Normal file
371
src/Functions.cpp
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
#include "EggDuino.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool g_bPrintSessionActive = false;
|
||||||
|
|
||||||
|
void captureSerialProtocolLine(const char *line)
|
||||||
|
{
|
||||||
|
captureIncomingPrintLine(line, PROTOCOL_TRANSPORT_SERIAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
void captureBleProtocolLine(const char *line)
|
||||||
|
{
|
||||||
|
captureIncomingPrintLine(line, PROTOCOL_TRANSPORT_BLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void captureWifiProtocolLine(const char *line)
|
||||||
|
{
|
||||||
|
captureIncomingPrintLine(line, PROTOCOL_TRANSPORT_WIFI);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void startPrintSessionIfNeeded()
|
||||||
|
{
|
||||||
|
if (!g_bPrintSessionActive)
|
||||||
|
{
|
||||||
|
clearIncomingPrintLog();
|
||||||
|
g_bPrintSessionActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopPrintSession()
|
||||||
|
{
|
||||||
|
g_bPrintSessionActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryPen()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
char state;
|
||||||
|
if (g_iPenState == g_iPenUpPos)
|
||||||
|
state = '1';
|
||||||
|
else
|
||||||
|
state = '0';
|
||||||
|
protocolWrite(String(state) + "\r\n");
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryButton()
|
||||||
|
{
|
||||||
|
protocolWrite(String(g_bPrgButtonState) + "\r\n");
|
||||||
|
sendAck();
|
||||||
|
g_bPrgButtonState = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryLayer()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
protocolWrite(String(g_uiLayer) + "\r\n");
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLayer()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
uint32_t value = 0;
|
||||||
|
char *arg1;
|
||||||
|
arg1 = nextCommandArg();
|
||||||
|
if (arg1 != NULL)
|
||||||
|
{
|
||||||
|
value = atoi(arg1);
|
||||||
|
g_uiLayer = value;
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryNodeCount()
|
||||||
|
{
|
||||||
|
protocolWrite(String(g_uiNodeCount) + "\r\n");
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNodeCount()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
uint32_t value = 0;
|
||||||
|
char *arg1;
|
||||||
|
arg1 = nextCommandArg();
|
||||||
|
if (arg1 != NULL)
|
||||||
|
{
|
||||||
|
value = atoi(arg1);
|
||||||
|
g_uiNodeCount = value;
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void nodeCountIncrement()
|
||||||
|
{
|
||||||
|
g_uiNodeCount++;
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void nodeCountDecrement()
|
||||||
|
{
|
||||||
|
g_uiNodeCount--;
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stepperMove()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
uint16_t duration = 0; // in ms
|
||||||
|
int penStepsEBB = 0; // Pen
|
||||||
|
int rotStepsEBB = 0; // Rot
|
||||||
|
|
||||||
|
if (!parseSMArgs(&duration, &penStepsEBB, &rotStepsEBB))
|
||||||
|
{
|
||||||
|
sendError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ((penStepsEBB == 0) && (rotStepsEBB == 0))
|
||||||
|
{
|
||||||
|
// delay(duration);
|
||||||
|
sendAck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startPrintSessionIfNeeded();
|
||||||
|
|
||||||
|
prepareMove(duration, penStepsEBB, rotStepsEBB);
|
||||||
|
moveToDestination();
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPen()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
int cmd;
|
||||||
|
int value;
|
||||||
|
char *arg;
|
||||||
|
char cstrMsg[20];
|
||||||
|
|
||||||
|
// moveToDestination();
|
||||||
|
|
||||||
|
arg = nextCommandArg();
|
||||||
|
if (arg != NULL)
|
||||||
|
{
|
||||||
|
cmd = atoi(arg);
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
movePenServoTo(g_iPenUpPos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
movePenServoTo(g_iPenDownPos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char *val;
|
||||||
|
val = nextCommandArg();
|
||||||
|
if (val != NULL)
|
||||||
|
{
|
||||||
|
value = atoi(val);
|
||||||
|
sendAck();
|
||||||
|
// delay(value);
|
||||||
|
}
|
||||||
|
if (val == NULL && arg != NULL)
|
||||||
|
{
|
||||||
|
sendAck();
|
||||||
|
// delay(500);
|
||||||
|
}
|
||||||
|
// Serial.println("delay");
|
||||||
|
if (val == NULL && arg == NULL)
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void togglePen()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
char *arg;
|
||||||
|
arg = nextCommandArg();
|
||||||
|
|
||||||
|
doTogglePen();
|
||||||
|
sendAck();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void doTogglePen()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
if (g_iPenState == g_iPenUpPos)
|
||||||
|
{
|
||||||
|
movePenServoTo(g_iPenDownPos);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
movePenServoTo(g_iPenUpPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableMotors()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
int cmd;
|
||||||
|
int value;
|
||||||
|
char *arg;
|
||||||
|
char *val;
|
||||||
|
arg = nextCommandArg();
|
||||||
|
if (arg != NULL)
|
||||||
|
cmd = atoi(arg);
|
||||||
|
val = nextCommandArg();
|
||||||
|
if (val != NULL)
|
||||||
|
value = atoi(val);
|
||||||
|
// values parsed
|
||||||
|
if ((arg != NULL) && (val == NULL))
|
||||||
|
{
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
stopPrintSession();
|
||||||
|
motorsOff();
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
startPrintSessionIfNeeded();
|
||||||
|
motorsOn();
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the following implementaion is a little bit cheated, because i did not know, how to implement different values for first and second argument.
|
||||||
|
if ((arg != NULL) && (val != NULL))
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
stopPrintSession();
|
||||||
|
motorsOff();
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
startPrintSessionIfNeeded();
|
||||||
|
motorsOn();
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stepperModeConfigure()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
int cmd;
|
||||||
|
int value;
|
||||||
|
int iTmpPenPos;
|
||||||
|
char *arg;
|
||||||
|
arg = nextCommandArg();
|
||||||
|
if (arg != NULL)
|
||||||
|
cmd = atoi(arg);
|
||||||
|
char *val;
|
||||||
|
val = nextCommandArg();
|
||||||
|
if (val != NULL)
|
||||||
|
value = atoi(val);
|
||||||
|
if ((arg != NULL) && (val != NULL))
|
||||||
|
{
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case 4:
|
||||||
|
iTmpPenPos = (int)((float)(value - 6000) / (float)133.3); // transformation from EBB to PWM-Servo
|
||||||
|
if (g_iPenDownPos != iTmpPenPos)
|
||||||
|
{
|
||||||
|
g_iPenDownPos = iTmpPenPos;
|
||||||
|
storePenDownPosInEE();
|
||||||
|
}
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
iTmpPenPos = (int)((float)(value - 6000) / (float)133.3); // transformation from EBB to PWM-Servo
|
||||||
|
if (g_iPenUpPos != iTmpPenPos)
|
||||||
|
{
|
||||||
|
g_iPenUpPos = iTmpPenPos;
|
||||||
|
storePenUpPosInEE();
|
||||||
|
}
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 6: // rotMin=value; ignored
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 7: // rotMax=value; ignored
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
g_iServoRateUp = value;
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
g_iServoRateDown = value;
|
||||||
|
sendAck();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendVersion()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
protocolWrite(initSting);
|
||||||
|
protocolWrite("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void unrecognized(const char *command)
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
sendError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ignore()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void makeComInterface()
|
||||||
|
{
|
||||||
|
auto registerCommands = [](SerialCommand &cmd) {
|
||||||
|
cmd.addCommand("v", sendVersion);
|
||||||
|
cmd.addCommand("EM", enableMotors);
|
||||||
|
cmd.addCommand("SC", stepperModeConfigure);
|
||||||
|
cmd.addCommand("SP", setPen);
|
||||||
|
cmd.addCommand("SM", stepperMove);
|
||||||
|
cmd.addCommand("SE", ignore);
|
||||||
|
cmd.addCommand("TP", togglePen);
|
||||||
|
cmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer
|
||||||
|
cmd.addCommand("NI", nodeCountIncrement);
|
||||||
|
cmd.addCommand("ND", nodeCountDecrement);
|
||||||
|
cmd.addCommand("SN", setNodeCount);
|
||||||
|
cmd.addCommand("QN", queryNodeCount);
|
||||||
|
cmd.addCommand("SL", setLayer);
|
||||||
|
cmd.addCommand("QL", queryLayer);
|
||||||
|
cmd.addCommand("QP", queryPen);
|
||||||
|
cmd.addCommand("QB", queryButton); //"PRG" Button,
|
||||||
|
cmd.setDefaultHandler(unrecognized); // Handler for command that isn't matched (says "What?")
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommands(SCmd);
|
||||||
|
SCmd.setLineHandler(captureSerialProtocolLine);
|
||||||
|
#ifdef ESP32
|
||||||
|
registerCommands(g_BLECmd);
|
||||||
|
g_BLECmd.setLineHandler(captureBleProtocolLine);
|
||||||
|
registerCommands(g_WifiCmd);
|
||||||
|
g_WifiCmd.setLineHandler(captureWifiProtocolLine);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
250
src/Helper_Functions.cpp
Normal file
250
src/Helper_Functions.cpp
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
#include "EggDuino.h"
|
||||||
|
#include "motion_planner.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
#ifdef ESP32
|
||||||
|
constexpr char kDeviceNamePrefix[] = "EggBot_";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int clampServoAngle(int angle)
|
||||||
|
{
|
||||||
|
if (angle < 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (angle > 180)
|
||||||
|
{
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint_fast16_t servoSpeedFromRate(int rate)
|
||||||
|
{
|
||||||
|
// EBB rate values are implementation-specific. We map them to ServoEasing degrees/second.
|
||||||
|
// Higher rate means faster movement.
|
||||||
|
if (rate <= 0)
|
||||||
|
{
|
||||||
|
return 70;
|
||||||
|
}
|
||||||
|
int speed = 20 + (rate / 2);
|
||||||
|
if (speed < 10)
|
||||||
|
{
|
||||||
|
speed = 10;
|
||||||
|
}
|
||||||
|
if (speed > 360)
|
||||||
|
{
|
||||||
|
speed = 360;
|
||||||
|
}
|
||||||
|
return (uint_fast16_t)speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
void buildDeviceName(char *nameBuffer, size_t bufferSize)
|
||||||
|
{
|
||||||
|
if ((nameBuffer == NULL) || (bufferSize == 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasmota-style chip ID uses the lower 24 bits of the ESP32 efuse MAC.
|
||||||
|
const uint32_t chipId = static_cast<uint32_t>(ESP.getEfuseMac() & 0xFFFFFFULL);
|
||||||
|
snprintf(nameBuffer, bufferSize, "%s%06X", kDeviceNamePrefix, chipId);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void updateStepCorrectionFactors()
|
||||||
|
{
|
||||||
|
if (g_iRotMicrostep <= 0)
|
||||||
|
{
|
||||||
|
g_iRotMicrostep = kDefaultRotMicrostep;
|
||||||
|
}
|
||||||
|
if (g_iPenMicrostep <= 0)
|
||||||
|
{
|
||||||
|
g_iPenMicrostep = kDefaultPenMicrostep;
|
||||||
|
}
|
||||||
|
fROT_STEP_CORRECTION = 16.0f / (float)g_iRotMicrostep;
|
||||||
|
fPEN_STEP_CORRECTION = 16.0f / (float)g_iPenMicrostep;
|
||||||
|
}
|
||||||
|
|
||||||
|
XYMotionPlanner *g_pPlanner = nullptr;
|
||||||
|
|
||||||
|
void initHardware()
|
||||||
|
{
|
||||||
|
if (!initConfigStore())
|
||||||
|
{
|
||||||
|
g_iPenUpPos = 5;
|
||||||
|
g_iPenDownPos = 20;
|
||||||
|
}
|
||||||
|
updateStepCorrectionFactors();
|
||||||
|
g_iPenState = g_iPenDownPos;
|
||||||
|
|
||||||
|
g_stepEngine.init();
|
||||||
|
g_pStepperRotate = g_stepEngine.stepperConnectToPin(g_iRotStepPin);
|
||||||
|
if (g_pStepperRotate)
|
||||||
|
{
|
||||||
|
// rotMotor.setMaxSpeed(2000.0);
|
||||||
|
// rotMotor.setAcceleration(10000.0);
|
||||||
|
|
||||||
|
g_pStepperRotate->setDirectionPin(g_iRotDirPin);
|
||||||
|
g_pStepperRotate->setEnablePin(g_iRotEnablePin);
|
||||||
|
g_pStepperRotate->setAcceleration(g_iMaxAcclSpeed);
|
||||||
|
g_pStepperRotate->setAutoEnable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stepper pen init
|
||||||
|
g_pStepperPen = g_stepEngine.stepperConnectToPin(g_iPenStepPin);
|
||||||
|
if (g_pStepperPen)
|
||||||
|
{
|
||||||
|
// penMotor.setMaxSpeed(2000.0);
|
||||||
|
// penMotor.setAcceleration(10000.0);
|
||||||
|
g_pStepperPen->setDirectionPin(g_iPenDirPin);
|
||||||
|
g_pStepperPen->setEnablePin(g_iPenEnablePin);
|
||||||
|
g_pStepperPen->setAcceleration(g_iMaxAcclSpeed);
|
||||||
|
g_pStepperPen->setAutoEnable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
motorsOff();
|
||||||
|
|
||||||
|
g_pPlanner = new XYMotionPlanner(g_pStepperPen, g_pStepperRotate);
|
||||||
|
g_pPlanner->setBaseLimits(4000, g_iMaxAcclSpeed);
|
||||||
|
g_pPlanner->setMinimums(50, 100);
|
||||||
|
g_pPlanner->setCurrentPosition(0, 0);
|
||||||
|
|
||||||
|
g_iPenState = clampServoAngle(g_iPenState);
|
||||||
|
penServo.attach(g_iServoPin, g_iPenState);
|
||||||
|
penServo.setEasingType(EASE_QUADRATIC_IN_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void movePenServoTo(int targetPos)
|
||||||
|
{
|
||||||
|
targetPos = clampServoAngle(targetPos);
|
||||||
|
int currentPos = clampServoAngle(g_iPenState);
|
||||||
|
if (currentPos == targetPos)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint_fast16_t speed = 0;
|
||||||
|
speed = servoSpeedFromRate(g_iServoRateDown);
|
||||||
|
penServo.easeTo(targetPos, speed);
|
||||||
|
|
||||||
|
g_iPenState = targetPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void storePenUpPosInEE()
|
||||||
|
{
|
||||||
|
saveConfigToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void storePenDownPosInEE()
|
||||||
|
{
|
||||||
|
saveConfigToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendAck()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
protocolWrite("OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendError()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
protocolWrite("unknown CMD\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void motorsOff()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
g_pStepperPen->disableOutputs();
|
||||||
|
g_pStepperRotate->disableOutputs();
|
||||||
|
g_bMotorsEnabled = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void motorsOn()
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
g_pStepperPen->enableOutputs();
|
||||||
|
g_pStepperRotate->enableOutputs();
|
||||||
|
g_bMotorsEnabled = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleMotors()
|
||||||
|
|
||||||
|
{
|
||||||
|
Log(__FUNCTION__);
|
||||||
|
if (g_bMotorsEnabled)
|
||||||
|
{
|
||||||
|
motorsOff();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
motorsOn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB)
|
||||||
|
{
|
||||||
|
char *arg1 = NULL;
|
||||||
|
char *arg2 = NULL;
|
||||||
|
char *arg3 = NULL;
|
||||||
|
arg1 = nextCommandArg();
|
||||||
|
if (arg1 != NULL)
|
||||||
|
{
|
||||||
|
*duration = atoi(arg1);
|
||||||
|
arg2 = nextCommandArg();
|
||||||
|
}
|
||||||
|
if (arg2 != NULL)
|
||||||
|
{
|
||||||
|
*penStepsEBB = atoi(arg2);
|
||||||
|
arg3 = nextCommandArg();
|
||||||
|
}
|
||||||
|
if (arg3 != NULL)
|
||||||
|
{
|
||||||
|
*rotStepsEBB = atoi(arg3);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareMove(uint16_t duration, int penStepsEBB, int rotStepsEBB)
|
||||||
|
{
|
||||||
|
if (!g_bMotorsEnabled)
|
||||||
|
{
|
||||||
|
motorsOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_pPlanner->moveBy(penStepsEBB / fPEN_STEP_CORRECTION, rotStepsEBB / fROT_STEP_CORRECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveOneStep()
|
||||||
|
{
|
||||||
|
Log("moveOneStep");
|
||||||
|
while (g_pStepperPen->isRunning() || g_pStepperRotate->isRunning() || g_pPlanner->isRunning())
|
||||||
|
{
|
||||||
|
g_pPlanner->update();
|
||||||
|
}
|
||||||
|
g_pPlanner->update();
|
||||||
|
Log("done");
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveToDestination()
|
||||||
|
{
|
||||||
|
Log("moveToDestination");
|
||||||
|
while (g_pStepperPen->isRunning() || g_pStepperRotate->isRunning() || g_pPlanner->isRunning())
|
||||||
|
{
|
||||||
|
g_pPlanner->update();
|
||||||
|
}
|
||||||
|
g_pPlanner->update();
|
||||||
|
Log("done");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setprgButtonState()
|
||||||
|
{
|
||||||
|
g_bPrgButtonState = 1;
|
||||||
|
}
|
||||||
152
src/Logging.cpp
Normal file
152
src/Logging.cpp
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#include "EggDuino.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr size_t kLogCapacity = 80;
|
||||||
|
constexpr size_t kLogLineLength = 160;
|
||||||
|
constexpr size_t kIncomingLogCapacity = 128;
|
||||||
|
constexpr size_t kIncomingLogLineLength = SERIALCOMMAND_BUFFER + 24;
|
||||||
|
|
||||||
|
char g_logLines[kLogCapacity][kLogLineLength];
|
||||||
|
uint32_t g_logSeq[kLogCapacity];
|
||||||
|
size_t g_logWritePos = 0;
|
||||||
|
uint32_t g_nextLogSeq = 1;
|
||||||
|
|
||||||
|
char g_incomingLogLines[kIncomingLogCapacity][kIncomingLogLineLength];
|
||||||
|
uint32_t g_incomingLogSeq[kIncomingLogCapacity];
|
||||||
|
size_t g_incomingLogWritePos = 0;
|
||||||
|
uint32_t g_nextIncomingLogSeq = 1;
|
||||||
|
|
||||||
|
void appendJsonEscaped(String &out, const char *text) {
|
||||||
|
out += "\"";
|
||||||
|
for (size_t i = 0; text[i] != '\0'; ++i) {
|
||||||
|
const char c = text[i];
|
||||||
|
switch (c) {
|
||||||
|
case '\\':
|
||||||
|
out += "\\\\";
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
out += "\\\"";
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
out += "\\n";
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
out += "\\r";
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
out += "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (static_cast<unsigned char>(c) < 0x20) {
|
||||||
|
out += '?';
|
||||||
|
} else {
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *transportLabel(ProtocolTransport transport) {
|
||||||
|
switch (transport) {
|
||||||
|
case PROTOCOL_TRANSPORT_BLE:
|
||||||
|
return "BLE";
|
||||||
|
case PROTOCOL_TRANSPORT_WIFI:
|
||||||
|
return "WIFI";
|
||||||
|
case PROTOCOL_TRANSPORT_SERIAL:
|
||||||
|
default:
|
||||||
|
return "SERIAL";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Log(const String &message) {
|
||||||
|
snprintf(
|
||||||
|
g_logLines[g_logWritePos],
|
||||||
|
kLogLineLength,
|
||||||
|
"[%010lu] %s",
|
||||||
|
static_cast<unsigned long>(millis()),
|
||||||
|
message.c_str()
|
||||||
|
);
|
||||||
|
g_logSeq[g_logWritePos] = g_nextLogSeq++;
|
||||||
|
g_logWritePos = (g_logWritePos + 1) % kLogCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Log(const char *message) {
|
||||||
|
Log(String(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildLogsJson(uint32_t sinceSeq) {
|
||||||
|
String output;
|
||||||
|
output.reserve(2048);
|
||||||
|
output += "{\"logs\":[";
|
||||||
|
uint32_t lastSeq = sinceSeq;
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < kLogCapacity; ++i) {
|
||||||
|
const size_t idx = (g_logWritePos + i) % kLogCapacity;
|
||||||
|
const uint32_t seq = g_logSeq[idx];
|
||||||
|
if (seq == 0 || seq <= sinceSeq) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
output += ",";
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
output += "{\"seq\":";
|
||||||
|
output += String(seq);
|
||||||
|
output += ",\"text\":";
|
||||||
|
appendJsonEscaped(output, g_logLines[idx]);
|
||||||
|
output += "}";
|
||||||
|
lastSeq = seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
output += "],\"lastSeq\":";
|
||||||
|
output += String(lastSeq);
|
||||||
|
output += "}";
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
void captureIncomingPrintLine(const char *line, ProtocolTransport transport) {
|
||||||
|
if ((line == NULL) || (line[0] == '\0')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(
|
||||||
|
g_incomingLogLines[g_incomingLogWritePos],
|
||||||
|
kIncomingLogLineLength,
|
||||||
|
"[%010lu] %s %s",
|
||||||
|
static_cast<unsigned long>(millis()),
|
||||||
|
transportLabel(transport),
|
||||||
|
line
|
||||||
|
);
|
||||||
|
g_incomingLogSeq[g_incomingLogWritePos] = g_nextIncomingLogSeq++;
|
||||||
|
g_incomingLogWritePos = (g_incomingLogWritePos + 1) % kIncomingLogCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildIncomingPrintLogText() {
|
||||||
|
String output;
|
||||||
|
output.reserve(4096);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < kIncomingLogCapacity; ++i) {
|
||||||
|
const size_t idx = (g_incomingLogWritePos + i) % kIncomingLogCapacity;
|
||||||
|
if (g_incomingLogSeq[idx] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output += g_incomingLogLines[idx];
|
||||||
|
output += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearIncomingPrintLog() {
|
||||||
|
for (size_t i = 0; i < kIncomingLogCapacity; ++i) {
|
||||||
|
g_incomingLogSeq[i] = 0;
|
||||||
|
g_incomingLogLines[i][0] = '\0';
|
||||||
|
}
|
||||||
|
g_incomingLogWritePos = 0;
|
||||||
|
g_nextIncomingLogSeq = 1;
|
||||||
|
}
|
||||||
156
src/WiFi_Protocol.cpp
Normal file
156
src/WiFi_Protocol.cpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#include "EggDuino.h"
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WebSocketsServer.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint16_t kWifiProtocolPort = 1337;
|
||||||
|
constexpr uint8_t kInvalidWifiClientId = 0xFF;
|
||||||
|
constexpr size_t kWifiRxQueueSize = 1024;
|
||||||
|
|
||||||
|
struct WifiRxByte
|
||||||
|
{
|
||||||
|
uint8_t value;
|
||||||
|
};
|
||||||
|
|
||||||
|
WebSocketsServer g_wifiProtocolSocket(kWifiProtocolPort);
|
||||||
|
WifiRxByte g_wifiRxQueue[kWifiRxQueueSize];
|
||||||
|
size_t g_wifiRxHead = 0;
|
||||||
|
size_t g_wifiRxTail = 0;
|
||||||
|
bool g_wifiRxOverflow = false;
|
||||||
|
bool g_wifiProtocolStarted = false;
|
||||||
|
uint8_t g_wifiProtocolClientId = kInvalidWifiClientId;
|
||||||
|
|
||||||
|
bool queueWifiByte(uint8_t value)
|
||||||
|
{
|
||||||
|
const size_t nextHead = (g_wifiRxHead + 1) % kWifiRxQueueSize;
|
||||||
|
if (nextHead == g_wifiRxTail)
|
||||||
|
{
|
||||||
|
g_wifiRxOverflow = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_wifiRxQueue[g_wifiRxHead].value = value;
|
||||||
|
g_wifiRxHead = nextHead;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dequeueWifiByte(WifiRxByte *byte)
|
||||||
|
{
|
||||||
|
if ((byte == NULL) || (g_wifiRxHead == g_wifiRxTail))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*byte = g_wifiRxQueue[g_wifiRxTail];
|
||||||
|
g_wifiRxTail = (g_wifiRxTail + 1) % kWifiRxQueueSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleWifiSocketEvent(uint8_t clientId, WStype_t type, uint8_t *payload, size_t length)
|
||||||
|
{
|
||||||
|
(void)payload;
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case WStype_CONNECTED:
|
||||||
|
if (g_wifiProtocolClientId == kInvalidWifiClientId)
|
||||||
|
{
|
||||||
|
g_wifiProtocolClientId = clientId;
|
||||||
|
}
|
||||||
|
Log(String("WiFi protocol client connected: ") + clientId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WStype_DISCONNECTED:
|
||||||
|
if (g_wifiProtocolClientId == clientId)
|
||||||
|
{
|
||||||
|
g_wifiProtocolClientId = kInvalidWifiClientId;
|
||||||
|
}
|
||||||
|
Log(String("WiFi protocol client disconnected: ") + clientId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WStype_TEXT:
|
||||||
|
case WStype_BIN:
|
||||||
|
if (g_wifiProtocolClientId == kInvalidWifiClientId)
|
||||||
|
{
|
||||||
|
g_wifiProtocolClientId = clientId;
|
||||||
|
}
|
||||||
|
if (clientId != g_wifiProtocolClientId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < length; ++i)
|
||||||
|
{
|
||||||
|
queueWifiByte(payload[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void startWifiProtocolInterface()
|
||||||
|
{
|
||||||
|
if (g_wifiProtocolStarted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_wifiProtocolSocket.begin();
|
||||||
|
g_wifiProtocolSocket.onEvent(handleWifiSocketEvent);
|
||||||
|
g_wifiProtocolStarted = true;
|
||||||
|
|
||||||
|
Log(String("WiFi EggBot protocol ws://") + WiFi.localIP().toString() + ":" + kWifiProtocolPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleWifiProtocolInterface()
|
||||||
|
{
|
||||||
|
if (!g_wifiProtocolStarted)
|
||||||
|
{
|
||||||
|
if (WiFi.status() == WL_CONNECTED)
|
||||||
|
{
|
||||||
|
startWifiProtocolInterface();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_wifiProtocolSocket.loop();
|
||||||
|
|
||||||
|
if (g_wifiRxOverflow)
|
||||||
|
{
|
||||||
|
g_wifiRxOverflow = false;
|
||||||
|
Log("WiFi protocol RX queue overflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
WifiRxByte byte = {0};
|
||||||
|
while (dequeueWifiByte(&byte))
|
||||||
|
{
|
||||||
|
setActiveProtocolContext(&g_WifiCmd, PROTOCOL_TRANSPORT_WIFI);
|
||||||
|
g_WifiCmd.readChar(static_cast<char>(byte.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifiProtocolWrite(const char *message)
|
||||||
|
{
|
||||||
|
if (!g_wifiProtocolStarted || (message == NULL))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((g_wifiProtocolClientId == kInvalidWifiClientId) || !g_wifiProtocolSocket.clientIsConnected(g_wifiProtocolClientId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return g_wifiProtocolSocket.sendTXT(g_wifiProtocolClientId, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
225
src/main.cpp
Normal file
225
src/main.cpp
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/* Eggduino-Firmware by Joachim Cerny, 2014
|
||||||
|
|
||||||
|
Thanks for the nice libs ACCELSTEPPER and SERIALCOMMAND, which made this project much easier.
|
||||||
|
Thanks to the Eggbot-Team for such a funny and enjoable concept!
|
||||||
|
Thanks to my wife and my daughter for their patience. :-)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// implemented Eggbot-Protocol-Version v13
|
||||||
|
// EBB-Command-Reference, I sourced from: http://www.schmalzhaus.com/EBB/EBBCommands.html
|
||||||
|
// no homing sequence, switch-on position of pen will be taken as reference point.
|
||||||
|
// No collision-detection!!
|
||||||
|
// Supported Servos: I do not know, I use Arduino Servo Lib with TG9e- standard servo.
|
||||||
|
// Note: Maximum-Speed in Inkscape is 1000 Steps/s. You could enter more, but then Pythonscript sends nonsense.
|
||||||
|
// EBB-Coordinates are coming in for 16th-Microstepmode. The Coordinate-Transforms are done in weired integer-math. Be careful, when you diecide to modify settings.
|
||||||
|
|
||||||
|
/* TODOs:
|
||||||
|
1 collision control via penMin/penMax
|
||||||
|
2 implement homing sequence via microswitch or optical device
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "EggDuino.h"
|
||||||
|
#include <ServoEasing.hpp>
|
||||||
|
|
||||||
|
// EXTRAFEATURES - UNCOMMENT TO USE THEM -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// #define prgButton 2 // PRG button
|
||||||
|
// #define penToggleButton 12 // pen up/down button
|
||||||
|
// #define motorsButton 4 // motors enable button
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
FastAccelStepperEngine g_stepEngine = FastAccelStepperEngine();
|
||||||
|
FastAccelStepper *g_pStepperRotate = NULL;
|
||||||
|
FastAccelStepper *g_pStepperPen = NULL;
|
||||||
|
|
||||||
|
// make Objects
|
||||||
|
ServoEasing penServo;
|
||||||
|
SerialCommand SCmd;
|
||||||
|
#ifdef ESP32
|
||||||
|
SerialCommand g_BLECmd;
|
||||||
|
SerialCommand g_WifiCmd;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
SerialCommand *g_pActiveParser = &SCmd;
|
||||||
|
ProtocolTransport g_activeTransport = PROTOCOL_TRANSPORT_SERIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create Buttons
|
||||||
|
#ifdef prgButton
|
||||||
|
Button prgButtonToggle(prgButton, setprgButtonState);
|
||||||
|
#endif
|
||||||
|
#ifdef penToggleButton
|
||||||
|
Button penToggle(penToggleButton, doTogglePen);
|
||||||
|
#endif
|
||||||
|
#ifdef motorsButton
|
||||||
|
Button motorsToggle(motorsButton, toggleMotors);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Variables... be careful, by messing around here, everything has a reason and crossrelations...
|
||||||
|
int g_iPenUpPos = 5; // can be overwritten from EBB-Command SC
|
||||||
|
int g_iPenDownPos = 20; // can be overwritten from EBB-Command SC
|
||||||
|
int g_iServoRateUp = 0; // from EBB-Protocol, mapped to ServoEasing speed (up movement)
|
||||||
|
int g_iServoRateDown = 0; // from EBB-Protocol, mapped to ServoEasing speed (down movement)
|
||||||
|
long g_iRotStepError = 0;
|
||||||
|
long g_iPenStepError = 0;
|
||||||
|
int g_iPenState = g_iPenUpPos;
|
||||||
|
uint32_t g_uiNodeCount = 0;
|
||||||
|
unsigned int g_uiLayer = 0;
|
||||||
|
boolean g_bPrgButtonState = 0;
|
||||||
|
boolean g_bMotorsEnabled = 0;
|
||||||
|
int g_iRotDirPin = kDefaultRotDirPin;
|
||||||
|
int g_iRotEnablePin = kDefaultRotEnablePin;
|
||||||
|
int g_iRotStepPin = kDefaultRotStepPin;
|
||||||
|
int g_iPenStepPin = kDefaultPenStepPin;
|
||||||
|
int g_iPenDirPin = kDefaultPenDirPin;
|
||||||
|
int g_iPenEnablePin = kDefaultPenEnablePin;
|
||||||
|
int g_iServoPin = kDefaultServoPin;
|
||||||
|
int g_iRotMicrostep = kDefaultRotMicrostep;
|
||||||
|
int g_iPenMicrostep = kDefaultPenMicrostep;
|
||||||
|
float fROT_STEP_CORRECTION = 16.0 / kDefaultRotMicrostep; // devide EBB-Coordinates by this factor to get EGGduino-Steps
|
||||||
|
float fPEN_STEP_CORRECTION = 16.0 / kDefaultPenMicrostep; // devide EBB-Coordinates by this factor to get EGGduino-Steps
|
||||||
|
int g_iMaxAcclSpeed = 10000;
|
||||||
|
String g_sHostname = "EggDuino";
|
||||||
|
String g_sWifiSsid = "";
|
||||||
|
String g_sWifiPassword = "";
|
||||||
|
|
||||||
|
void setActiveProtocolContext(SerialCommand *parser, ProtocolTransport transport)
|
||||||
|
{
|
||||||
|
if (parser != NULL)
|
||||||
|
{
|
||||||
|
g_pActiveParser = parser;
|
||||||
|
}
|
||||||
|
g_activeTransport = transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *nextCommandArg()
|
||||||
|
{
|
||||||
|
return g_pActiveParser->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
void protocolWrite(const char *message)
|
||||||
|
{
|
||||||
|
if (message == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
if (g_activeTransport == PROTOCOL_TRANSPORT_BLE)
|
||||||
|
{
|
||||||
|
if (bleProtocolWrite(message))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g_activeTransport == PROTOCOL_TRANSPORT_WIFI)
|
||||||
|
{
|
||||||
|
if (wifiProtocolWrite(message))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Serial.print(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void protocolWrite(const String &message)
|
||||||
|
{
|
||||||
|
protocolWrite(message.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stepper Test
|
||||||
|
#ifdef TEST
|
||||||
|
// #define dirPinStepper 16
|
||||||
|
// #define enablePinStepper 12
|
||||||
|
// #define stepPinStepper 26
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("Starting...");
|
||||||
|
startWebInterface();
|
||||||
|
makeComInterface();
|
||||||
|
initHardware();
|
||||||
|
startBleInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t g_uiState = 0;
|
||||||
|
unsigned long g_uiLastTim = millis();
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
#ifdef TEST
|
||||||
|
|
||||||
|
unsigned long uiNow = millis();
|
||||||
|
motorsOn();
|
||||||
|
|
||||||
|
if (uiNow - g_uiLastTim > 5000)
|
||||||
|
{
|
||||||
|
g_uiLastTim = uiNow;
|
||||||
|
switch (g_uiState)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
Log(String(g_uiState));
|
||||||
|
g_pStepperRotate->setSpeedInUs(10); // the parameter is us/step !!!
|
||||||
|
g_pStepperRotate->setAcceleration(10000);
|
||||||
|
g_pStepperRotate->move(1000);
|
||||||
|
g_uiState++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
Log(String(g_uiState));
|
||||||
|
g_pStepperRotate->setSpeedInUs(10); // the parameter is us/step !!!
|
||||||
|
g_pStepperRotate->setAcceleration(10000);
|
||||||
|
g_pStepperRotate->move(-1000);
|
||||||
|
g_uiState++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
Log(String(g_uiState));
|
||||||
|
g_pStepperPen->setSpeedInUs(10); // the parameter is us/step !!!
|
||||||
|
g_pStepperPen->setAcceleration(10000);
|
||||||
|
g_pStepperPen->move(1000);
|
||||||
|
g_uiState++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
Log(String(g_uiState));
|
||||||
|
g_pStepperPen->setSpeedInUs(10); // the parameter is us/step !!!
|
||||||
|
g_pStepperPen->setAcceleration(10000);
|
||||||
|
g_pStepperPen->move(-1000);
|
||||||
|
g_uiState = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Log("Alive");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// moveOneStep();
|
||||||
|
setActiveProtocolContext(&SCmd, PROTOCOL_TRANSPORT_SERIAL);
|
||||||
|
SCmd.readSerial();
|
||||||
|
handleBleInterface();
|
||||||
|
handleWebInterface();
|
||||||
|
handleWifiProtocolInterface();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef penToggleButton
|
||||||
|
penToggle.check();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef motorsButton
|
||||||
|
motorsToggle.check();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef prgButton
|
||||||
|
prgButtonToggle.check();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
141
src/motion_planner.cpp
Normal file
141
src/motion_planner.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "motion_planner.h"
|
||||||
|
|
||||||
|
XYMotionPlanner::XYMotionPlanner(FastAccelStepper* stepperX, FastAccelStepper* stepperY)
|
||||||
|
: m_x(stepperX), m_y(stepperY) {}
|
||||||
|
|
||||||
|
void XYMotionPlanner::setBaseLimits(uint32_t maxSpeedHz, uint32_t maxAccelStepsPerS2) {
|
||||||
|
m_baseSpeedHz = max<uint32_t>(1, maxSpeedHz);
|
||||||
|
m_baseAccel = max<uint32_t>(1, maxAccelStepsPerS2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XYMotionPlanner::setMinimums(uint32_t minSpeedHz, uint32_t minAccelStepsPerS2) {
|
||||||
|
m_minSpeedHz = minSpeedHz;
|
||||||
|
m_minAccel = minAccelStepsPerS2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XYMotionPlanner::setCurrentPosition(int32_t x, int32_t y) {
|
||||||
|
m_currentX = x;
|
||||||
|
m_currentY = y;
|
||||||
|
m_targetX = x;
|
||||||
|
m_targetY = y;
|
||||||
|
if (m_x) {
|
||||||
|
m_x->setCurrentPosition(x);
|
||||||
|
}
|
||||||
|
if (m_y) {
|
||||||
|
m_y->setCurrentPosition(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t XYMotionPlanner::currentX() const { return m_currentX; }
|
||||||
|
|
||||||
|
int32_t XYMotionPlanner::currentY() const { return m_currentY; }
|
||||||
|
|
||||||
|
bool XYMotionPlanner::isRunning() const {
|
||||||
|
bool rx = m_x ? m_x->isRunning() : false;
|
||||||
|
bool ry = m_y ? m_y->isRunning() : false;
|
||||||
|
return rx || ry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XYMotionPlanner::waitUntilFinished() {
|
||||||
|
while (isRunning()) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
m_currentX = m_targetX;
|
||||||
|
m_currentY = m_targetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XYMotionPlanner::moveTo(int32_t targetX, int32_t targetY) {
|
||||||
|
if ((m_x == nullptr) || (m_y == nullptr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep planner state in sync when the previous move already ended,
|
||||||
|
// but update() has not been called by the caller yet.
|
||||||
|
update();
|
||||||
|
|
||||||
|
m_targetX = targetX;
|
||||||
|
m_targetY = targetY;
|
||||||
|
|
||||||
|
int32_t dxSigned = m_targetX - m_currentX;
|
||||||
|
int32_t dySigned = m_targetY - m_currentY;
|
||||||
|
|
||||||
|
uint32_t dx = (dxSigned >= 0) ? dxSigned : -dxSigned;
|
||||||
|
uint32_t dy = (dySigned >= 0) ? dySigned : -dySigned;
|
||||||
|
|
||||||
|
if ((dx == 0) && (dy == 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t dmax = max(dx, dy);
|
||||||
|
|
||||||
|
// Skalierungsfaktoren relativ zur dominanten Achse
|
||||||
|
float sx = (float)dx / (float)dmax;
|
||||||
|
float sy = (float)dy / (float)dmax;
|
||||||
|
|
||||||
|
// Dominante Achse bekommt die Basiswerte
|
||||||
|
uint32_t speedX = scaledValue(m_baseSpeedHz, sx, dx);
|
||||||
|
uint32_t speedY = scaledValue(m_baseSpeedHz, sy, dy);
|
||||||
|
uint32_t accelX = scaledValue(m_baseAccel, sx, dx);
|
||||||
|
uint32_t accelY = scaledValue(m_baseAccel, sy, dy);
|
||||||
|
|
||||||
|
// Kurze Achsen mit Bewegung sollen nicht auf 0 fallen
|
||||||
|
if (dx > 0) {
|
||||||
|
speedX = max(speedX, m_minSpeedHz);
|
||||||
|
accelX = max(accelX, m_minAccel);
|
||||||
|
}
|
||||||
|
if (dy > 0) {
|
||||||
|
speedY = max(speedY, m_minSpeedHz);
|
||||||
|
accelY = max(accelY, m_minAccel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FastAccelStepper: speed in steps/s, accel in steps/s²
|
||||||
|
if (dx > 0) {
|
||||||
|
m_x->setSpeedInHz(speedX);
|
||||||
|
m_x->setAcceleration(accelX);
|
||||||
|
}
|
||||||
|
if (dy > 0) {
|
||||||
|
m_y->setSpeedInHz(speedY);
|
||||||
|
m_y->setAcceleration(accelY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bewegungen anstoßen
|
||||||
|
// Reihenfolge ist hier meist ausreichend.
|
||||||
|
// Für "noch synchroner" müsste man tiefer in die Queue-API gehen.
|
||||||
|
if (dx > 0) {
|
||||||
|
m_x->moveTo(m_targetX);
|
||||||
|
}
|
||||||
|
if (dy > 0) {
|
||||||
|
m_y->moveTo(m_targetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XYMotionPlanner::moveBy(int32_t deltaX, int32_t deltaY) {
|
||||||
|
update();
|
||||||
|
|
||||||
|
// Chain relative moves reliably:
|
||||||
|
// - if running, append relative to the current planned target
|
||||||
|
// - if idle, use the current position
|
||||||
|
const int32_t baseX = isRunning() ? m_targetX : m_currentX;
|
||||||
|
const int32_t baseY = isRunning() ? m_targetY : m_currentY;
|
||||||
|
return moveTo(baseX + deltaX, baseY + deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XYMotionPlanner::update() {
|
||||||
|
if (!isRunning()) {
|
||||||
|
m_currentX = m_targetX;
|
||||||
|
m_currentY = m_targetY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t XYMotionPlanner::scaledValue(uint32_t base, float scale, uint32_t distance) {
|
||||||
|
if (distance == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
float v = (float)base * scale;
|
||||||
|
if (v < 1.0f) {
|
||||||
|
v = 1.0f;
|
||||||
|
}
|
||||||
|
return (uint32_t)(v + 0.5f);
|
||||||
|
}
|
||||||
161
svg/holger.svg
Normal file
161
svg/holger.svg
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 3200 1024"
|
||||||
|
width="3200"
|
||||||
|
height="1024"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg6435"
|
||||||
|
sodipodi:docname="holger.svg"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs6439">
|
||||||
|
<rect
|
||||||
|
x="527.87384"
|
||||||
|
y="605.33038"
|
||||||
|
width="1085.6606"
|
||||||
|
height="402.98206"
|
||||||
|
id="rect7043" />
|
||||||
|
<rect
|
||||||
|
x="656.15399"
|
||||||
|
y="649.70941"
|
||||||
|
width="550.19073"
|
||||||
|
height="212.89944"
|
||||||
|
id="rect7037" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6437"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.22269721"
|
||||||
|
inkscape:cx="222.2749"
|
||||||
|
inkscape:cy="624.16589"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1008"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6435" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:date>2026-02-26T20:25:37.207Z</dc:date>
|
||||||
|
<dc:creator>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:rights>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Copyright 2026 André Fiedler</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:rights>
|
||||||
|
<dc:publisher>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:publisher>
|
||||||
|
<dc:identifier>eggi-1652987163-1.3.40</dc:identifier>
|
||||||
|
<dc:source>https://eggbot.app/</dc:source>
|
||||||
|
<dc:relation>https://eggbot.app/</dc:relation>
|
||||||
|
<dc:language>de</dc:language>
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Bag>
|
||||||
|
<rdf:li>sorbian</rdf:li>
|
||||||
|
<rdf:li>eggbot</rdf:li>
|
||||||
|
<rdf:li>ornament</rdf:li>
|
||||||
|
<rdf:li>version-1.3.40</rdf:li>
|
||||||
|
</rdf:Bag>
|
||||||
|
</dc:subject>
|
||||||
|
<dc:coverage>2048x1024px</dc:coverage>
|
||||||
|
<dc:description>Generated with EggBot Sorbische Eier using eggbot-app 1.3.40</dc:description>
|
||||||
|
<dc:contributor>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:contributor>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
aria-label="Holger's"
|
||||||
|
transform="matrix(1.9550597,0,0,0.6333943,-339.16516,-218.21761)"
|
||||||
|
id="text6983"
|
||||||
|
style="font-size:267.284px;vector-effect:non-scaling-stroke;stroke:#000000;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline">
|
||||||
|
<path
|
||||||
|
d="m 337.82058,591.41632 h -24.05556 v -89.00557 h -97.82594 v 89.00557 H 191.88352 V 400.57555 h 24.05556 v 80.71976 h 97.82594 v -80.71976 h 24.05556 z"
|
||||||
|
id="path7791"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 511.28769,519.51693 q 0,35.54877 -18.17531,55.0605 -17.90803,19.51173 -48.64569,19.51173 -18.97716,0 -33.94507,-8.55309 -14.70062,-8.82037 -23.25371,-25.39198 -8.55308,-16.83889 -8.55308,-40.62716 0,-35.54878 17.90802,-54.79322 17.90803,-19.24445 48.64569,-19.24445 19.51173,0 34.21235,8.82037 14.96791,8.55309 23.25371,25.1247 8.55309,16.30432 8.55309,40.0926 z m -108.25002,0 q 0,25.39198 9.88951,40.35988 10.15679,14.70062 32.07408,14.70062 21.65,0 31.80679,-14.70062 10.15679,-14.9679 10.15679,-40.35988 0,-25.39198 -10.15679,-39.82532 -10.15679,-14.43334 -32.07408,-14.43334 -21.91728,0 -31.80679,14.43334 -9.88951,14.43334 -9.88951,39.82532 z"
|
||||||
|
id="path7793"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="M 571.96102,591.41632 H 548.44003 V 388.28048 h 23.52099 z"
|
||||||
|
id="path7795"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 668.18312,445.47926 q 14.16605,0 25.39198,5.34568 11.49321,5.34568 19.51173,16.30432 h 1.33642 l 3.20741,-18.97716 h 18.70988 v 145.66978 q 0,30.73766 -15.76975,46.24013 -15.50248,15.50247 -48.37841,15.50247 -31.53951,0 -51.58581,-9.08766 v -21.65 q 21.11544,11.22593 52.92223,11.22593 18.4426,0 28.86667,-10.95865 10.69136,-10.69136 10.69136,-29.40124 v -5.61296 q 0,-3.20741 0.26729,-9.08766 0.26728,-6.14753 0.53457,-8.55308 h -1.06914 q -14.43334,21.65 -44.36914,21.65 -27.79754,0 -43.5673,-19.51173 -15.50247,-19.51173 -15.50247,-54.52594 0,-34.21235 15.50247,-54.25865 15.76976,-20.31358 43.30001,-20.31358 z m 3.20741,19.77901 q -17.90803,0 -27.79754,14.43334 -9.8895,14.16605 -9.8895,40.62717 0,26.46111 9.62222,40.62717 9.88951,13.89876 28.59939,13.89876 21.65,0 31.53951,-11.49321 9.88951,-11.76049 9.88951,-37.68704 v -5.61297 q 0,-29.40124 -10.15679,-41.96358 -10.1568,-12.82964 -31.8068,-12.82964 z"
|
||||||
|
id="path7797"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 837.10656,445.47926 q 18.44259,0 31.53951,8.01852 13.3642,8.01852 20.31358,22.71914 7.21667,14.43333 7.21667,33.94507 v 14.16605 h -98.09323 q 0.53457,24.32284 12.29507,37.15247 12.02778,12.56235 33.4105,12.56235 13.63148,0 24.05556,-2.40555 10.69136,-2.67284 21.91729,-7.48396 v 20.58087 q -10.95865,4.81111 -21.65001,6.94938 -10.69136,2.40556 -25.39198,2.40556 -20.31358,0 -36.08334,-8.2858 -15.50247,-8.28581 -24.32284,-24.59013 -8.55309,-16.57161 -8.55309,-40.35988 0,-23.521 7.75124,-40.35989 8.01852,-16.83889 22.18457,-25.92655 14.43333,-9.08765 33.4105,-9.08765 z m -0.26729,19.24445 q -16.83889,0 -26.7284,10.95864 -9.62222,10.69136 -11.49321,29.93581 h 72.96853 q -0.26728,-18.17531 -8.55308,-29.40124 -8.28581,-11.49321 -26.19384,-11.49321 z"
|
||||||
|
id="path7799"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 999.3477,445.47926 q 4.0093,0 8.5531,0.53457 4.8111,0.26728 8.2858,1.06913 l -2.9401,21.65001 q -3.4747,-0.80186 -7.7513,-1.33642 -4.0092,-0.53457 -7.75121,-0.53457 -10.95864,0 -20.58087,6.14753 -9.62222,5.88025 -15.50247,16.83889 -5.61296,10.69136 -5.61296,25.1247 v 76.44322 H 932.5267 V 448.1521 h 19.24444 l 2.67284,26.19383 h 1.06914 q 6.94938,-11.7605 17.90803,-20.31358 10.95864,-8.55309 25.92655,-8.55309 z"
|
||||||
|
id="path7801"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 1073.653,400.57555 -5.3457,68.95927 h -14.7006 l -5.3457,-68.95927 z"
|
||||||
|
id="path7803"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 1199.0091,551.85829 q 0,20.84815 -15.5025,31.53951 -15.5025,10.69136 -41.6963,10.69136 -14.9679,0 -25.9266,-2.40556 -10.6913,-2.40555 -18.9771,-6.6821 v -21.38271 q 8.5531,4.27654 20.5808,8.01852 12.2951,3.47469 24.8575,3.47469 17.908,0 25.9265,-5.61297 8.0185,-5.88024 8.0185,-15.50247 0,-5.34568 -2.9401,-9.62222 -2.9401,-4.27655 -10.6914,-8.55309 -7.4839,-4.27654 -21.65,-9.62222 -13.8987,-5.34568 -23.7882,-10.69136 -9.8895,-5.34568 -15.2352,-12.82964 -5.3457,-7.48395 -5.3457,-19.24444 0,-18.17532 14.7006,-28.06482 14.9679,-9.88951 39.0235,-9.88951 13.0969,0 24.3228,2.67284 11.4932,2.40555 21.3827,6.94938 l -8.0185,18.70988 q -9.0876,-3.74197 -18.9771,-6.41481 -9.8895,-2.67284 -20.3136,-2.67284 -14.4334,0 -22.1846,4.81111 -7.4839,4.54383 -7.4839,12.56235 0,5.88024 3.4746,10.15679 3.4747,4.00926 11.4933,8.01852 8.2858,3.74197 21.9172,9.08765 13.6315,5.0784 23.2538,10.42408 9.6222,5.34568 14.7006,13.09692 5.0784,7.48395 5.0784,18.97716 z"
|
||||||
|
id="path7805"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
aria-label="Egg"
|
||||||
|
transform="matrix(2.8193627,0,0,1.1226094,4.5464998,-430.01989)"
|
||||||
|
id="text7049"
|
||||||
|
style="font-size:411.886px;vector-effect:non-scaling-stroke;stroke:#000000;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline">
|
||||||
|
<path
|
||||||
|
d="M 563.96012,1117.9706 H 399.61761 V 823.88399 h 164.34251 v 32.53899 H 436.68735 v 91.85057 h 119.85882 v 32.12711 H 436.68735 v 105.03094 h 127.27277 z"
|
||||||
|
id="path7808"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 701.94183,893.08083 q 21.82996,0 39.12917,8.23772 17.7111,8.23772 30.06768,25.12505 h 2.05943 l 4.94263,-29.24391 h 28.83202 v 224.47791 q 0,47.3668 -24.30127,71.2562 -23.88939,23.8894 -74.55137,23.8894 -48.60254,0 -79.49399,-14.0041 v -33.3628 q 32.53899,17.2992 81.55342,17.2992 28.42014,0 44.48369,-16.8873 16.47544,-16.4754 16.47544,-45.3074 v -8.6496 q 0,-4.9427 0.41189,-14.0042 0.41188,-9.4734 0.82377,-13.1803 h -1.64755 q -22.24184,33.3627 -68.37307,33.3627 -42.83614,0 -67.13742,-30.0676 -23.88938,-30.0677 -23.88938,-84.0248 0,-52.72138 23.88938,-83.61283 24.30128,-31.30334 66.72553,-31.30334 z m 4.94264,30.47957 q -27.59637,0 -42.83615,22.24184 -15.23978,21.82996 -15.23978,62.60666 0,40.7767 14.8279,62.6067 15.23978,21.418 44.0718,21.418 33.36276,0 48.60254,-17.711 15.23979,-18.123 15.23979,-58.076 v -8.6496 q 0,-45.30744 -15.65167,-64.66608 -15.65167,-19.77052 -49.01443,-19.77052 z"
|
||||||
|
id="path7810"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
<path
|
||||||
|
d="m 955.25162,893.08083 q 21.82995,0 39.12917,8.23772 17.71111,8.23772 30.06771,25.12505 h 2.0594 l 4.9426,-29.24391 h 28.832 v 224.47791 q 0,47.3668 -24.3012,71.2562 -23.8894,23.8894 -74.55139,23.8894 -48.60255,0 -79.494,-14.0041 v -33.3628 q 32.53899,17.2992 81.55343,17.2992 28.42013,0 44.48366,-16.8873 16.4755,-16.4754 16.4755,-45.3074 v -8.6496 q 0,-4.9427 0.4118,-14.0042 0.4119,-9.4734 0.8238,-13.1803 h -1.6475 q -22.2419,33.3627 -68.3731,33.3627 -42.83614,0 -67.13741,-30.0676 -23.88939,-30.0677 -23.88939,-84.0248 0,-52.72138 23.88939,-83.61283 24.30127,-31.30334 66.72553,-31.30334 z m 4.94263,30.47957 q -27.59636,0 -42.83614,22.24184 -15.23978,21.82996 -15.23978,62.60666 0,40.7767 14.82789,62.6067 15.23978,21.418 44.0718,21.418 33.36277,0 48.60258,-17.711 15.2397,-18.123 15.2397,-58.076 v -8.6496 q 0,-45.30744 -15.6516,-64.66608 -15.65169,-19.77052 -49.01445,-19.77052 z"
|
||||||
|
id="path7812"
|
||||||
|
style="vector-effect:non-scaling-stroke;stroke-width:1;stroke-dasharray:none;-inkscape-stroke:hairline" />
|
||||||
|
</g>
|
||||||
|
<eggbot
|
||||||
|
layer="12345"
|
||||||
|
node="0"
|
||||||
|
lastpath="0"
|
||||||
|
lastpathnc="0"
|
||||||
|
totaldeltax="0"
|
||||||
|
totaldeltay="0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
979
svg/rect.svg
Normal file
979
svg/rect.svg
Normal file
@@ -0,0 +1,979 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 3200 1024"
|
||||||
|
width="3200"
|
||||||
|
height="1024"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg6435"
|
||||||
|
sodipodi:docname="rect.svg"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs6439">
|
||||||
|
<rect
|
||||||
|
x="527.87384"
|
||||||
|
y="605.33038"
|
||||||
|
width="1085.6606"
|
||||||
|
height="402.98206"
|
||||||
|
id="rect7043" />
|
||||||
|
<rect
|
||||||
|
x="656.15399"
|
||||||
|
y="649.70941"
|
||||||
|
width="550.19073"
|
||||||
|
height="212.89944"
|
||||||
|
id="rect7037" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6437"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.31494141"
|
||||||
|
inkscape:cx="1592.3597"
|
||||||
|
inkscape:cy="650.91473"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1008"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6435" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:date>2026-02-26T20:25:37.207Z</dc:date>
|
||||||
|
<dc:creator>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:rights>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Copyright 2026 André Fiedler</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:rights>
|
||||||
|
<dc:publisher>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:publisher>
|
||||||
|
<dc:identifier>eggi-1652987163-1.3.40</dc:identifier>
|
||||||
|
<dc:source>https://eggbot.app/</dc:source>
|
||||||
|
<dc:relation>https://eggbot.app/</dc:relation>
|
||||||
|
<dc:language>de</dc:language>
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Bag>
|
||||||
|
<rdf:li>sorbian</rdf:li>
|
||||||
|
<rdf:li>eggbot</rdf:li>
|
||||||
|
<rdf:li>ornament</rdf:li>
|
||||||
|
<rdf:li>version-1.3.40</rdf:li>
|
||||||
|
</rdf:Bag>
|
||||||
|
</dc:subject>
|
||||||
|
<dc:coverage>2048x1024px</dc:coverage>
|
||||||
|
<dc:description>Generated with EggBot Sorbische Eier using eggbot-app 1.3.40</dc:description>
|
||||||
|
<dc:contributor>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:contributor>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<eggbot
|
||||||
|
layer="12345"
|
||||||
|
node="0"
|
||||||
|
lastpath="0"
|
||||||
|
lastpathnc="0"
|
||||||
|
totaldeltax="0"
|
||||||
|
totaldeltay="0" />
|
||||||
|
<rect
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-width:0.468004;stroke-dasharray:none;-inkscape-stroke:hairline"
|
||||||
|
id="rect33437"
|
||||||
|
width="12.822335"
|
||||||
|
height="959.19885"
|
||||||
|
x="40.943718"
|
||||||
|
y="35.52866"
|
||||||
|
inkscape:tile-cx="47.354886"
|
||||||
|
inkscape:tile-cy="515.12809"
|
||||||
|
inkscape:tile-w="12.822335"
|
||||||
|
inkscape:tile-h="959.19885"
|
||||||
|
inkscape:tile-x0="40.943718"
|
||||||
|
inkscape:tile-y0="35.52866" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
id="use37434" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(25.683137)"
|
||||||
|
id="use37436" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(51.366274)"
|
||||||
|
id="use37438" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(77.049411)"
|
||||||
|
id="use37440" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(102.73255)"
|
||||||
|
id="use37442" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(128.41569)"
|
||||||
|
id="use37444" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(154.09882)"
|
||||||
|
id="use37446" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(179.78196)"
|
||||||
|
id="use37448" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(205.4651)"
|
||||||
|
id="use37450" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(231.14823)"
|
||||||
|
id="use37452" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(256.83137)"
|
||||||
|
id="use37454" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(282.51451)"
|
||||||
|
id="use37456" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(308.19764)"
|
||||||
|
id="use37458" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(333.88078)"
|
||||||
|
id="use37460" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(359.56392)"
|
||||||
|
id="use37462" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(385.24706)"
|
||||||
|
id="use37464" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(410.93019)"
|
||||||
|
id="use37466" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(436.61333)"
|
||||||
|
id="use37468" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(462.29647)"
|
||||||
|
id="use37470" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(487.9796)"
|
||||||
|
id="use37472" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(513.66274)"
|
||||||
|
id="use37474" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(539.34588)"
|
||||||
|
id="use37476" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(565.02901)"
|
||||||
|
id="use37478" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(590.71215)"
|
||||||
|
id="use37480" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(616.39529)"
|
||||||
|
id="use37482" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(642.07843)"
|
||||||
|
id="use37484" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(667.76156)"
|
||||||
|
id="use37486" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(693.4447)"
|
||||||
|
id="use37488" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(719.12784)"
|
||||||
|
id="use37490" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(744.81097)"
|
||||||
|
id="use37492" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(770.49411)"
|
||||||
|
id="use37494" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(796.17725)"
|
||||||
|
id="use37496" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(821.86038)"
|
||||||
|
id="use37498" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(847.54352)"
|
||||||
|
id="use37500" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(873.22666)"
|
||||||
|
id="use37502" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(898.9098)"
|
||||||
|
id="use37504" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(924.59293)"
|
||||||
|
id="use37506" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(950.27607)"
|
||||||
|
id="use37508" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(975.95921)"
|
||||||
|
id="use37510" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1001.6423)"
|
||||||
|
id="use37512" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1027.3255)"
|
||||||
|
id="use37514" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1053.0086)"
|
||||||
|
id="use37516" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1078.6918)"
|
||||||
|
id="use37518" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1104.3749)"
|
||||||
|
id="use37520" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1130.058)"
|
||||||
|
id="use37522" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1155.7412)"
|
||||||
|
id="use37524" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1181.4243)"
|
||||||
|
id="use37526" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1207.1074)"
|
||||||
|
id="use37528" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1232.7906)"
|
||||||
|
id="use37530" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1258.4737)"
|
||||||
|
id="use37532" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1284.1569)"
|
||||||
|
id="use37534" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1309.84)"
|
||||||
|
id="use37536" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1335.5231)"
|
||||||
|
id="use37538" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1361.2063)"
|
||||||
|
id="use37540" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1386.8894)"
|
||||||
|
id="use37542" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1412.5725)"
|
||||||
|
id="use37544" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1438.2557)"
|
||||||
|
id="use37546" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1463.9388)"
|
||||||
|
id="use37548" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1489.6219)"
|
||||||
|
id="use37550" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1515.3051)"
|
||||||
|
id="use37552" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1540.9882)"
|
||||||
|
id="use37554" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1566.6714)"
|
||||||
|
id="use37556" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1592.3545)"
|
||||||
|
id="use37558" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1618.0376)"
|
||||||
|
id="use37560" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1643.7208)"
|
||||||
|
id="use37562" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1669.4039)"
|
||||||
|
id="use37564" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1695.087)"
|
||||||
|
id="use37566" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1720.7702)"
|
||||||
|
id="use37568" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1746.4533)"
|
||||||
|
id="use37570" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1772.1365)"
|
||||||
|
id="use37572" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1797.8196)"
|
||||||
|
id="use37574" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1823.5027)"
|
||||||
|
id="use37576" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1849.1859)"
|
||||||
|
id="use37578" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1874.869)"
|
||||||
|
id="use37580" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1900.5521)"
|
||||||
|
id="use37582" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1926.2353)"
|
||||||
|
id="use37584" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1951.9184)"
|
||||||
|
id="use37586" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(1977.6015)"
|
||||||
|
id="use37588" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2003.2847)"
|
||||||
|
id="use37590" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2028.9678)"
|
||||||
|
id="use37592" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2054.651)"
|
||||||
|
id="use37594" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2080.3341)"
|
||||||
|
id="use37596" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2106.0172)"
|
||||||
|
id="use37598" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2131.7004)"
|
||||||
|
id="use37600" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2157.3835)"
|
||||||
|
id="use37602" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2183.0666)"
|
||||||
|
id="use37604" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2208.7498)"
|
||||||
|
id="use37606" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2234.4329)"
|
||||||
|
id="use37608" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2260.1161)"
|
||||||
|
id="use37610" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2285.7992)"
|
||||||
|
id="use37612" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2311.4823)"
|
||||||
|
id="use37614" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2337.1655)"
|
||||||
|
id="use37616" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2362.8486)"
|
||||||
|
id="use37618" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2388.5317)"
|
||||||
|
id="use37620" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2414.2149)"
|
||||||
|
id="use37622" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2439.898)"
|
||||||
|
id="use37624" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2465.5812)"
|
||||||
|
id="use37626" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2491.2643)"
|
||||||
|
id="use37628" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2516.9474)"
|
||||||
|
id="use37630" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2542.6306)"
|
||||||
|
id="use37632" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2568.3137)"
|
||||||
|
id="use37634" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2593.9968)"
|
||||||
|
id="use37636" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2619.68)"
|
||||||
|
id="use37638" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2645.3631)"
|
||||||
|
id="use37640" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2671.0462)"
|
||||||
|
id="use37642" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2696.7294)"
|
||||||
|
id="use37644" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2722.4125)"
|
||||||
|
id="use37646" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2748.0957)"
|
||||||
|
id="use37648" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2773.7788)"
|
||||||
|
id="use37650" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2799.4619)"
|
||||||
|
id="use37652" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2825.1451)"
|
||||||
|
id="use37654" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2850.8282)"
|
||||||
|
id="use37656" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2876.5113)"
|
||||||
|
id="use37658" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2902.1945)"
|
||||||
|
id="use37660" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2927.8776)"
|
||||||
|
id="use37662" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2953.5608)"
|
||||||
|
id="use37664" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(2979.2439)"
|
||||||
|
id="use37666" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(3004.927)"
|
||||||
|
id="use37668" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(3030.6102)"
|
||||||
|
id="use37670" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(3056.2933)"
|
||||||
|
id="use37672" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(3081.9764)"
|
||||||
|
id="use37674" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(3107.6596)"
|
||||||
|
id="use37676" />
|
||||||
|
<use
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
inkscape:tiled-clone-of="#rect33437"
|
||||||
|
xlink:href="#rect33437"
|
||||||
|
transform="translate(3133.3427)"
|
||||||
|
id="use37678" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
375
svg/test-stars.svg
Normal file
375
svg/test-stars.svg
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 3200 1024"
|
||||||
|
width="3200"
|
||||||
|
height="1024"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg6435"
|
||||||
|
sodipodi:docname="test-stars.svg"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs6439">
|
||||||
|
<rect
|
||||||
|
x="527.87384"
|
||||||
|
y="605.33038"
|
||||||
|
width="1085.6606"
|
||||||
|
height="402.98206"
|
||||||
|
id="rect7043" />
|
||||||
|
<rect
|
||||||
|
x="656.15399"
|
||||||
|
y="649.70941"
|
||||||
|
width="550.19073"
|
||||||
|
height="212.89944"
|
||||||
|
id="rect7037" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6437"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.22269721"
|
||||||
|
inkscape:cx="226.7653"
|
||||||
|
inkscape:cy="624.16588"
|
||||||
|
inkscape:window-width="3840"
|
||||||
|
inkscape:window-height="2088"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6435" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:date>2026-02-26T20:25:37.207Z</dc:date>
|
||||||
|
<dc:creator>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:rights>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Copyright 2026 André Fiedler</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:rights>
|
||||||
|
<dc:publisher>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:publisher>
|
||||||
|
<dc:identifier>eggi-1652987163-1.3.40</dc:identifier>
|
||||||
|
<dc:source>https://eggbot.app/</dc:source>
|
||||||
|
<dc:relation>https://eggbot.app/</dc:relation>
|
||||||
|
<dc:language>de</dc:language>
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Bag>
|
||||||
|
<rdf:li>sorbian</rdf:li>
|
||||||
|
<rdf:li>eggbot</rdf:li>
|
||||||
|
<rdf:li>ornament</rdf:li>
|
||||||
|
<rdf:li>version-1.3.40</rdf:li>
|
||||||
|
</rdf:Bag>
|
||||||
|
</dc:subject>
|
||||||
|
<dc:coverage>2048x1024px</dc:coverage>
|
||||||
|
<dc:description>Generated with EggBot Sorbische Eier using eggbot-app 1.3.40</dc:description>
|
||||||
|
<dc:contributor>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>EggBot Sorbische Eier</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:contributor>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<eggbot
|
||||||
|
layer="0"
|
||||||
|
node="0"
|
||||||
|
lastpath="0"
|
||||||
|
lastpathnc="0"
|
||||||
|
totaldeltax="0"
|
||||||
|
totaldeltay="0" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="39.552366"
|
||||||
|
inkscape:transform-center-y="3.0857472"
|
||||||
|
transform="matrix(1.5873511,0,0,1.5286783,1415.4845,238.82281)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,776.59278,667.72404)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,708.01302,147.69442)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-5"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,284.23914,674.5927)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,215.65938,154.56308)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-56"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,2807.9794,657.35363)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-2"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,2739.3997,137.32401)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-5-9"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,2315.6258,664.22229)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="18.782775"
|
||||||
|
inkscape:transform-center-y="1.4067283"
|
||||||
|
transform="matrix(0.75380592,0,0,0.6968924,2247.046,144.19267)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1-2"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="11.70076"
|
||||||
|
inkscape:transform-center-y="0.76651195"
|
||||||
|
transform="matrix(0.46958664,0,0,0.37972871,2079.8392,436.14535)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1-2-7"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="11.70076"
|
||||||
|
inkscape:transform-center-y="0.76651195"
|
||||||
|
transform="matrix(0.46958664,0,0,0.37972871,1822.022,810.93276)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1-2-7-0"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="11.70076"
|
||||||
|
inkscape:transform-center-y="0.76651195"
|
||||||
|
transform="matrix(0.46958664,0,0,0.37972871,2537.4397,462.2151)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1-2-7-9"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="11.70076"
|
||||||
|
inkscape:transform-center-y="0.76651195"
|
||||||
|
transform="matrix(0.46958664,0,0,0.37972871,1210.9897,703.77539)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1-2-7-3"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="11.70076"
|
||||||
|
inkscape:transform-center-y="0.76651195"
|
||||||
|
transform="matrix(0.46958664,0,0,0.37972871,1186.805,194.11559)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="vector-effect:non-scaling-stroke;fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:bevel;-inkscape-stroke:hairline;paint-order:fill markers stroke"
|
||||||
|
id="path1577-3-7-3-1-2-7-6"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
sodipodi:cx="143.94885"
|
||||||
|
sodipodi:cy="162.33527"
|
||||||
|
sodipodi:r1="281.11676"
|
||||||
|
sodipodi:r2="140.55838"
|
||||||
|
sodipodi:arg1="0.60507974"
|
||||||
|
sodipodi:arg2="1.2333983"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 375.15523,322.24226 190.47831,294.96885 63.314968,431.63957 32.185209,247.57344 -137.092,168.86749 28.345668,82.381768 50.889936,-102.9319 184.2658,27.683114 367.47614,-8.141086 284.46928,159.06916 Z"
|
||||||
|
inkscape:transform-center-x="11.70076"
|
||||||
|
inkscape:transform-center-y="0.76651195"
|
||||||
|
transform="matrix(0.46958664,0,0,0.37972871,558.24411,451.62824)" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
83
tests/test_package_firmware.py
Normal file
83
tests/test_package_firmware.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from scripts.package_firmware import build_manifest, extract_firmware_version, package_bundle
|
||||||
|
|
||||||
|
|
||||||
|
class PackageFirmwareTests(unittest.TestCase):
|
||||||
|
def test_extract_firmware_version_reads_init_string(self):
|
||||||
|
header_text = '#define initSting "EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a"\n'
|
||||||
|
|
||||||
|
self.assertEqual(extract_firmware_version(header_text), "1.6a")
|
||||||
|
|
||||||
|
def test_build_manifest_matches_expected_esp32_offsets(self):
|
||||||
|
manifest = build_manifest("1.6a")
|
||||||
|
|
||||||
|
self.assertEqual(manifest["name"], "EggDuino ESP32 Firmware")
|
||||||
|
self.assertEqual(manifest["version"], "1.6a")
|
||||||
|
self.assertTrue(manifest["new_install_prompt_erase"])
|
||||||
|
self.assertEqual(manifest["builds"][0]["chipFamily"], "ESP32")
|
||||||
|
self.assertEqual(
|
||||||
|
manifest["builds"][0]["parts"],
|
||||||
|
[
|
||||||
|
{"path": "bootloader.bin", "offset": 4096},
|
||||||
|
{"path": "partitions.bin", "offset": 32768},
|
||||||
|
{"path": "boot_app0.bin", "offset": 57344},
|
||||||
|
{"path": "firmware.bin", "offset": 65536},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_package_bundle_copies_outputs_and_removes_stale_files(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
temp_path = Path(temp_dir)
|
||||||
|
build_dir = temp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
header_path = temp_path / "EggDuino.h"
|
||||||
|
output_dir = temp_path / "firmware"
|
||||||
|
output_dir.mkdir()
|
||||||
|
boot_app0_path = temp_path / "boot_app0.bin"
|
||||||
|
|
||||||
|
build_artifacts = {
|
||||||
|
"bootloader.bin": "bootloader",
|
||||||
|
"partitions.bin": "partitions",
|
||||||
|
"firmware.bin": "firmware-bin",
|
||||||
|
"firmware.elf": "firmware-elf",
|
||||||
|
"firmware.map": "firmware-map",
|
||||||
|
}
|
||||||
|
for file_name, contents in build_artifacts.items():
|
||||||
|
(build_dir / file_name).write_text(contents, encoding="utf-8")
|
||||||
|
|
||||||
|
header_path.write_text(
|
||||||
|
'#define initSting "EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6b"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
boot_app0_path.write_text("boot-app0", encoding="utf-8")
|
||||||
|
(output_dir / "stale.txt").write_text("old", encoding="utf-8")
|
||||||
|
|
||||||
|
package_bundle(
|
||||||
|
build_dir=build_dir,
|
||||||
|
boot_app0_path=boot_app0_path,
|
||||||
|
header_path=header_path,
|
||||||
|
output_dir=output_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse((output_dir / "stale.txt").exists())
|
||||||
|
expected_files = {
|
||||||
|
"bootloader.bin": "bootloader",
|
||||||
|
"partitions.bin": "partitions",
|
||||||
|
"firmware.bin": "firmware-bin",
|
||||||
|
"firmware.elf": "firmware-elf",
|
||||||
|
"firmware.map": "firmware-map",
|
||||||
|
"boot_app0.bin": "boot-app0",
|
||||||
|
}
|
||||||
|
for file_name, contents in expected_files.items():
|
||||||
|
self.assertEqual((output_dir / file_name).read_text(encoding="utf-8"), contents)
|
||||||
|
|
||||||
|
manifest = json.loads((output_dir / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(manifest["version"], "1.6b")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user