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
|
||||
.settings/
|
||||
.loadpath
|
||||
.vscode
|
||||
.pio
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
@@ -213,3 +215,4 @@ pip-log.txt
|
||||
|
||||
#Mr Developer
|
||||
.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
|
||||
- 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),
|
||||
commandCount(0),
|
||||
defaultHandler(NULL),
|
||||
lineHandler(NULL),
|
||||
term('\r'), // default terminator for commands, newline character
|
||||
last(NULL)
|
||||
{
|
||||
@@ -64,6 +65,10 @@ void SerialCommand::setDefaultHandler(void (*function)(const char *)) {
|
||||
defaultHandler = function;
|
||||
}
|
||||
|
||||
void SerialCommand::setLineHandler(LineHandler function) {
|
||||
lineHandler = function;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
void SerialCommand::readSerial() {
|
||||
while (Serial.available() > 0) {
|
||||
char inChar = Serial.read(); // Read single available character, there may be more waiting
|
||||
readSerial(Serial);
|
||||
}
|
||||
|
||||
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
|
||||
Serial.print(inChar); // Echo back to serial stream
|
||||
#endif
|
||||
@@ -83,6 +97,10 @@ void SerialCommand::readSerial() {
|
||||
Serial.println(buffer);
|
||||
#endif
|
||||
|
||||
if ((lineHandler != NULL) && (bufPos > 0)) {
|
||||
(*lineHandler)(buffer);
|
||||
}
|
||||
|
||||
char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
|
||||
if (command != NULL) {
|
||||
boolean matched = false;
|
||||
@@ -113,8 +131,7 @@ void SerialCommand::readSerial() {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
buffer[bufPos++] = inChar; // Put character into buffer
|
||||
buffer[bufPos] = '\0'; // Null terminate
|
||||
@@ -124,7 +141,6 @@ void SerialCommand::readSerial() {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -44,11 +44,16 @@
|
||||
|
||||
class SerialCommand {
|
||||
public:
|
||||
typedef void (*LineHandler)(const char *);
|
||||
|
||||
SerialCommand(); // Constructor
|
||||
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 setLineHandler(LineHandler function); // Optional handler called with each completed input line.
|
||||
|
||||
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.
|
||||
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
|
||||
void (*defaultHandler)(const char *);
|
||||
LineHandler lineHandler;
|
||||
|
||||
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')
|
||||
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