diff --git a/.gitignore b/.gitignore
index b9d6bd9..80da7d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..200f812
--- /dev/null
+++ b/AGENTS.md
@@ -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.
diff --git a/AccelStepper.cpp b/AccelStepper.cpp
deleted file mode 100644
index 29e6469..0000000
--- a/AccelStepper.cpp
+++ /dev/null
@@ -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);
- }
-}
diff --git a/AccelStepper.h b/AccelStepper.h
deleted file mode 100644
index 26f9436..0000000
--- a/AccelStepper.h
+++ /dev/null
@@ -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
\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.
-/// \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.
-/// \version 1.44 examples/DualMotorShield/DualMotorShield.ino examples/DualMotorShield/DualMotorShield.pde
-/// was missing from the distribution.
-/// \version 1.45 Fixed a problem where if setAcceleration was not called, there was no default
-/// acceleration. Reported by Michael Newman.
-/// \version 1.45 Fixed inaccuracy in acceleration rate by using Equation 15, suggested by Sebastian Gracki.
-/// Performance improvements in runSpeed suggested by Jaakko Fagerlund.
-/// \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.
-
-///
-/// \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
-#if ARDUINO >= 100
-#include
-#else
-#include
-#include
-#endif
-
-// These defs cause trouble on some versions of Arduino
-#undef round
-
-/////////////////////////////////////////////////////////////////////
-/// \class AccelStepper 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
diff --git a/EggDuino.ino b/EggDuino.ino
deleted file mode 100644
index 9e7b642..0000000
--- a/EggDuino.ino
+++ /dev/null
@@ -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
-#include "SerialCommand.h" //nice lib from Stefan Rado, https://github.com/kroimon/Arduino-SerialCommand
-#include
-#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
-}
diff --git a/Functions.ino b/Functions.ino
deleted file mode 100644
index 6ca5960..0000000
--- a/Functions.ino
+++ /dev/null
@@ -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();
-}
diff --git a/Helper_Functions.ino b/Helper_Functions.ino
deleted file mode 100644
index b3b5cb0..0000000
--- a/Helper_Functions.ino
+++ /dev/null
@@ -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;
-}
diff --git a/README.md b/README.md
index b5be555..3f216ba 100644
--- a/README.md
+++ b/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;
+
diff --git a/docs/eggbot-ble-serial-protocol.md b/docs/eggbot-ble-serial-protocol.md
new file mode 100644
index 0000000..c34f998
--- /dev/null
+++ b/docs/eggbot-ble-serial-protocol.md
@@ -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: `\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.
diff --git a/docs/eggbot-serial-protocol.md b/docs/eggbot-serial-protocol.md
new file mode 100644
index 0000000..5224874
--- /dev/null
+++ b/docs/eggbot-serial-protocol.md
@@ -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,[,]`
+- `SC,,`
+- `SP,[,]`
+- `SM,,,`
+- `SE` (ack-only placeholder)
+- `TP[,]`
+- `PO` (ack-only placeholder)
+- `NI`
+- `ND`
+- `SN,`
+- `QN`
+- `SL,`
+- `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.
diff --git a/docs/eggbot-wifi-serial-protocol.md b/docs/eggbot-wifi-serial-protocol.md
new file mode 100644
index 0000000..0b0ef76
--- /dev/null
+++ b/docs/eggbot-wifi-serial-protocol.md
@@ -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://: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: `\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.
diff --git a/docs/examples/eggbot-ble-client.mjs b/docs/examples/eggbot-ble-client.mjs
new file mode 100644
index 0000000..079dd81
--- /dev/null
+++ b/docs/examples/eggbot-ble-client.mjs
@@ -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);
+}
diff --git a/docs/examples/eggbot-wifi-client.mjs b/docs/examples/eggbot-wifi-client.mjs
new file mode 100644
index 0000000..9f5dd43
--- /dev/null
+++ b/docs/examples/eggbot-wifi-client.mjs
@@ -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);
+}
diff --git a/include/EggDuino.h b/include/EggDuino.h
new file mode 100644
index 0000000..ab089ad
--- /dev/null
+++ b/include/EggDuino.h
@@ -0,0 +1,152 @@
+#ifndef EGGDUINO_H
+#define EGGDUINO_H
+
+#include
+#ifdef ESP32
+#include
+#include
+#include
+#include
+#else
+#include
+#endif
+
+#include
+#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
+#define dir1 16
+#define enableRotMotor 12
+#define step1 26
+#define rotMicrostep 32
+
+// Pen Stepper
+#define step2 25
+#define dir2 27
+#define enablePenMotor 12
+#define penMicrostep 32
+
+#define servoPin 17
+
+
+#else
+
+// Rotational Stepper
+#define step1 2
+#define dir1 5
+#define enableRotMotor 8
+#define rotMicrostep 16
+
+// Pen Stepper
+#define step2 3
+#define dir2 6
+#define enablePenMotor 8
+#define penMicrostep 16
+
+#define servoPin 4
+
+
+#endif
+
+struct ConfigParameter {
+ const char *key;
+ int *value;
+ String description;
+ int defaultValue;
+};
+
+extern FastAccelStepperEngine g_stepEngine;
+extern FastAccelStepper *g_pStepperRotate;
+extern FastAccelStepper *g_pStepperPen;
+
+extern Servo 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;
+
+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();
+
+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 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 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);
+
+#endif
diff --git a/button.h b/include/button.h
similarity index 100%
rename from button.h
rename to include/button.h
diff --git a/inkscape/linux/AxiDraw_395_LinX86.zip b/inkscape/linux/AxiDraw_395_LinX86.zip
new file mode 100644
index 0000000..5eb9cba
Binary files /dev/null and b/inkscape/linux/AxiDraw_395_LinX86.zip differ
diff --git a/inkscape/linux/patch/ebb_serial.py b/inkscape/linux/patch/ebb_serial.py
new file mode 100644
index 0000000..64082ac
--- /dev/null
+++ b/inkscape/linux/patch/ebb_serial.py
@@ -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)
diff --git a/inkscape/linux/readme.md b/inkscape/linux/readme.md
new file mode 100644
index 0000000..b196f89
--- /dev/null
+++ b/inkscape/linux/readme.md
@@ -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.
\ No newline at end of file
diff --git a/SerialCommand.cpp b/lib/SerialCommand/src/SerialCommand.cpp
similarity index 64%
rename from SerialCommand.cpp
rename to lib/SerialCommand/src/SerialCommand.cpp
index 9d0e7af..a14c1a7 100644
--- a/SerialCommand.cpp
+++ b/lib/SerialCommand/src/SerialCommand.cpp
@@ -71,58 +71,65 @@ 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
+
+ if (inChar == term) { // Check for the terminator (default '\r') meaning end of command
#ifdef SERIALCOMMAND_DEBUG
- Serial.print(inChar); // Echo back to serial stream
+ Serial.print("Received: ");
+ Serial.println(buffer);
#endif
- if (inChar == term) { // Check for the terminator (default '\r') meaning end of command
- #ifdef SERIALCOMMAND_DEBUG
- Serial.print("Received: ");
- Serial.println(buffer);
- #endif
+ char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
+ if (command != NULL) {
+ boolean matched = false;
+ for (int i = 0; i < commandCount; i++) {
+ #ifdef SERIALCOMMAND_DEBUG
+ Serial.print("Comparing [");
+ Serial.print(command);
+ Serial.print("] to [");
+ Serial.print(commandList[i].command);
+ Serial.println("]");
+ #endif
- char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
- if (command != NULL) {
- boolean matched = false;
- for (int i = 0; i < commandCount; i++) {
+ // Compare the found command against the list of known commands for a match
+ if (strncmp(command, commandList[i].command, SERIALCOMMAND_MAXCOMMANDLENGTH) == 0) {
#ifdef SERIALCOMMAND_DEBUG
- Serial.print("Comparing [");
- Serial.print(command);
- Serial.print("] to [");
- Serial.print(commandList[i].command);
- Serial.println("]");
+ Serial.print("Matched Command: ");
+ Serial.println(command);
#endif
- // Compare the found command against the list of known commands for a match
- if (strncmp(command, commandList[i].command, SERIALCOMMAND_MAXCOMMANDLENGTH) == 0) {
- #ifdef SERIALCOMMAND_DEBUG
- Serial.print("Matched Command: ");
- Serial.println(command);
- #endif
-
- // Execute the stored handler function for the command
- (*commandList[i].function)();
- matched = true;
- break;
- }
- }
- if (!matched && (defaultHandler != NULL)) {
- (*defaultHandler)(command);
+ // Execute the stored handler function for the command
+ (*commandList[i].function)();
+ matched = true;
+ break;
}
}
- clearBuffer();
+ if (!matched && (defaultHandler != NULL)) {
+ (*defaultHandler)(command);
+ }
}
- 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
- } else {
- #ifdef SERIALCOMMAND_DEBUG
- Serial.println("Line buffer is full - increase SERIALCOMMAND_BUFFER");
- #endif
- }
+ clearBuffer();
+ } 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
+ } else {
+ #ifdef SERIALCOMMAND_DEBUG
+ Serial.println("Line buffer is full - increase SERIALCOMMAND_BUFFER");
+ #endif
}
}
}
diff --git a/SerialCommand.h b/lib/SerialCommand/src/SerialCommand.h
similarity index 94%
rename from SerialCommand.h
rename to lib/SerialCommand/src/SerialCommand.h
index 9f98c38..d52308e 100644
--- a/SerialCommand.h
+++ b/lib/SerialCommand/src/SerialCommand.h
@@ -49,6 +49,8 @@ class SerialCommand {
void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received.
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).
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..16dfa7b
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,38 @@
+; 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*
+lib_deps =
+ 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_speed = 115200
+upload_port = /dev/cu.usb*
+lib_deps =
+ 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
diff --git a/src/BLE_Interface.cpp b/src/BLE_Interface.cpp
new file mode 100644
index 0000000..cb488cc
--- /dev/null
+++ b/src/BLE_Interface.cpp
@@ -0,0 +1,243 @@
+#include "EggDuino.h"
+
+#ifdef ESP32
+#include
+#include
+
+namespace
+{
+constexpr char kBleDeviceNamePrefix[] = "EggDuino_";
+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);
+}
+
+void buildBleDeviceName(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(ESP.getEfuseMac() & 0xFFFFFFULL);
+ snprintf(nameBuffer, bufferSize, "%s%06X", kBleDeviceNamePrefix, chipId);
+}
+
+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(value[i]));
+ }
+ }
+};
+} // namespace
+
+void startBleInterface()
+{
+ char bleDeviceName[32] = {0};
+ buildBleDeviceName(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(value));
+ }
+}
+
+bool bleProtocolWrite(const char *message)
+{
+ if ((message == NULL) || !g_bleClientConnected || (g_pBleTxCharacteristic == NULL))
+ {
+ return false;
+ }
+
+ const uint8_t *payload = reinterpret_cast(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
diff --git a/src/Config_Web.cpp b/src/Config_Web.cpp
new file mode 100644
index 0000000..c8e7b1d
--- /dev/null
+++ b/src/Config_Web.cpp
@@ -0,0 +1,414 @@
+#include "EggDuino.h"
+#include
+#include "credentials.h"
+
+namespace
+{
+ const char *kConfigPath = "/config.json";
+
+ WebServer server(80);
+ bool configStoreReady = false;
+
+ 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)
+ {
+ *configParameters[i].value = configParameters[i].defaultValue;
+ }
+ }
+
+ void handleRoot()
+ {
+ static const char kPage[] PROGMEM = R"HTML(
+
+
+
+
+
+EggDuino Konfiguration
+
+
+
+
+