mirror of
https://github.com/bdring/Grbl_Esp32.git
synced 2025-09-02 02:42:36 +02:00
Implemented more configuration code.
This commit is contained in:
18
Grbl_Esp32/src/Configuration/Configurable.h
Normal file
18
Grbl_Esp32/src/Configuration/Configurable.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "Generator.h"
|
||||
#include "Parser.h"
|
||||
|
||||
namespace Configuration
|
||||
{
|
||||
class Configurable
|
||||
{
|
||||
Configurable(const Configurable&) = delete;
|
||||
Configurable& operator=(const Configurable&) = delete;
|
||||
|
||||
public:
|
||||
Configurable() = default;
|
||||
|
||||
virtual void generate(Generator& generator) const = 0;
|
||||
};
|
||||
}
|
72
Grbl_Esp32/src/Configuration/Generator.cpp
Normal file
72
Grbl_Esp32/src/Configuration/Generator.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "Generator.h"
|
||||
|
||||
#include "Configurable.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Configuration
|
||||
{
|
||||
void Generator::enter(const char* name)
|
||||
{
|
||||
indent();
|
||||
addStr(name);
|
||||
addStr(":\n");
|
||||
indent_++;
|
||||
}
|
||||
|
||||
void Generator::add(const char* key, const std::string& value)
|
||||
{
|
||||
add(key, value.c_str());
|
||||
}
|
||||
|
||||
void Generator::add(const char* key, const char* value)
|
||||
{
|
||||
indent();
|
||||
addStr(key);
|
||||
addStr(": ");
|
||||
addStr(value);
|
||||
addStr("\n");
|
||||
}
|
||||
|
||||
void Generator::add(const char* key, bool value)
|
||||
{
|
||||
if (value) { add(key, "true"); }
|
||||
else { add(key, "false"); }
|
||||
}
|
||||
|
||||
void Generator::add(const char* key, int value)
|
||||
{
|
||||
char tmp[11];
|
||||
snprintf(tmp, 11, "%d", value);
|
||||
add(key, tmp);
|
||||
}
|
||||
|
||||
void Generator::add(const char* key, double value)
|
||||
{
|
||||
char tmp[20];
|
||||
snprintf(tmp, 20, "%f", value);
|
||||
add(key, tmp);
|
||||
}
|
||||
|
||||
void Generator::add(const char* key, Pin value)
|
||||
{
|
||||
if (!value.undefined())
|
||||
{
|
||||
add(key, value.str());
|
||||
}
|
||||
}
|
||||
|
||||
void Generator::add(Configuration::Configurable* configurable)
|
||||
{
|
||||
if (configurable != nullptr)
|
||||
{
|
||||
configurable->generate(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void Generator::leave()
|
||||
{
|
||||
addStr("\n");
|
||||
indent_--;
|
||||
}
|
||||
}
|
51
Grbl_Esp32/src/Configuration/Generator.h
Normal file
51
Grbl_Esp32/src/Configuration/Generator.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Pin.h"
|
||||
|
||||
namespace Configuration
|
||||
{
|
||||
class Configurable;
|
||||
|
||||
class Generator
|
||||
{
|
||||
Generator(const Generator&) = delete;
|
||||
Generator& operator=(const Generator&) = delete;
|
||||
|
||||
std::vector<char> config_;
|
||||
int indent_;
|
||||
|
||||
inline void addStr(const char* text) {
|
||||
for (auto it = text; *it; ++it)
|
||||
{
|
||||
config_.push_back(*it);
|
||||
}
|
||||
}
|
||||
|
||||
inline void indent() {
|
||||
for (int i = 0; i < indent_ * 2; ++i)
|
||||
{
|
||||
config_.push_back(' ');
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Generator() = default;
|
||||
|
||||
void enter(const char* name);
|
||||
void add(const char* key, const std::string& value);
|
||||
void add(const char* key, const char* value);
|
||||
void add(const char* key, bool value);
|
||||
void add(const char* key, int value);
|
||||
void add(const char* key, double value);
|
||||
void add(const char* key, Pin value);
|
||||
void add(Configuration::Configurable* configurable);
|
||||
void leave();
|
||||
|
||||
inline std::string str() const {
|
||||
return std::string(config_.begin(), config_.end());
|
||||
}
|
||||
};
|
||||
}
|
75
Grbl_Esp32/src/Configuration/GenericFactory.h
Normal file
75
Grbl_Esp32/src/Configuration/GenericFactory.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Configuration {
|
||||
template <typename BaseType>
|
||||
class GenericFactory
|
||||
{
|
||||
static GenericFactory& instance() {
|
||||
static GenericFactory instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
GenericFactory() = default;
|
||||
|
||||
GenericFactory(const GenericFactory&) = delete;
|
||||
GenericFactory& operator=(const GenericFactory&) = delete;
|
||||
|
||||
class BuilderBase {
|
||||
const char* name_;
|
||||
|
||||
public:
|
||||
BuilderBase(const char* name) : name_(name) {}
|
||||
|
||||
BuilderBase(const BuilderBase& o) = delete;
|
||||
BuilderBase& operator=(const BuilderBase& o) = delete;
|
||||
|
||||
virtual BaseType* create(Configuration::Parser& parser) const = 0;
|
||||
|
||||
inline bool matches(const char* name) {
|
||||
return !strcmp(name, name_);
|
||||
}
|
||||
|
||||
virtual ~BuilderBase() = default;
|
||||
};
|
||||
|
||||
std::vector<BuilderBase*> builders_;
|
||||
|
||||
inline static void registerBuilder(BuilderBase* builder)
|
||||
{
|
||||
instance().builders_.push_back(builder);
|
||||
}
|
||||
|
||||
public:
|
||||
template <typename DerivedType>
|
||||
class InstanceBuilder : public BuilderBase
|
||||
{
|
||||
public:
|
||||
InstanceBuilder(const char* name) : BuilderBase(name) {
|
||||
instance().registerBuilder(this);
|
||||
}
|
||||
|
||||
BaseType* create(Configuration::Parser& parser) const override
|
||||
{
|
||||
return new DerivedType(parser);
|
||||
}
|
||||
};
|
||||
|
||||
static const BuilderBase* find(const char* name) {
|
||||
for (auto it : instance().builders_)
|
||||
{
|
||||
if (it->matches(name))
|
||||
{
|
||||
return it;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline static const BuilderBase* find(const std::string& name) {
|
||||
return find(name.c_str());
|
||||
}
|
||||
};
|
||||
}
|
@@ -11,7 +11,7 @@ namespace Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
void Parser::ParseError(const char* description) const {
|
||||
void Parser::parseError(const char* description) const {
|
||||
// Attempt to use the correct position in the parser:
|
||||
if (current_.keyEnd_) {
|
||||
throw ParseException(start_, current_.keyEnd_, description);
|
||||
@@ -25,7 +25,7 @@ namespace Configuration {
|
||||
/// MoveNext: moves to the next entry in the current section. By default we're in the
|
||||
/// root section.
|
||||
/// </summary>
|
||||
bool Parser::MoveNext() {
|
||||
bool Parser::moveNext() {
|
||||
// While the indent of the token is > current indent, we have to skip it. This is a
|
||||
// sub-section, that we're apparently not interested in.
|
||||
while (token_.indent_ > current_.indent_) {
|
||||
@@ -48,7 +48,7 @@ namespace Configuration {
|
||||
return current_.kind_ != TokenKind::Eof;
|
||||
}
|
||||
|
||||
void Parser::Enter() {
|
||||
void Parser::enter() {
|
||||
indentStack_.push(current_.indent_);
|
||||
|
||||
// If we can enter, token_.indent_ > current_.indent_:
|
||||
@@ -59,9 +59,10 @@ namespace Configuration {
|
||||
current_ = TokenData();
|
||||
current_.indent_ = INT_MAX;
|
||||
}
|
||||
indent_ = current_.indent_;
|
||||
}
|
||||
|
||||
void Parser::Leave() {
|
||||
void Parser::leave() {
|
||||
// While the indent of the tokenizer is >= current, we can ignore the contents:
|
||||
while (token_.indent_ >= current_.indent_ && token_.kind_ != TokenKind::Eof) {
|
||||
Tokenize();
|
||||
@@ -70,6 +71,7 @@ namespace Configuration {
|
||||
// At this point, we just know the indent is smaller. We don't know if we're in
|
||||
// the *right* section tho.
|
||||
auto last = indentStack_.top();
|
||||
indent_ = last;
|
||||
indentStack_.pop();
|
||||
|
||||
if (last == token_.indent_) {
|
||||
@@ -82,31 +84,40 @@ namespace Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
std::string Parser::StringValue() const {
|
||||
std::string Parser::stringValue() const {
|
||||
if (current_.kind_ != TokenKind::String) {
|
||||
ParseError("Expected a string value (e.g. 'foo')");
|
||||
parseError("Expected a string value (e.g. 'foo')");
|
||||
}
|
||||
return std::string(current_.sValueStart_, current_.sValueEnd_);
|
||||
}
|
||||
|
||||
bool Parser::BoolValue() const {
|
||||
bool Parser::boolValue() const {
|
||||
if (current_.kind_ != TokenKind::Boolean) {
|
||||
ParseError("Expected a boolean value (e.g. true or value)");
|
||||
parseError("Expected a boolean value (e.g. true or value)");
|
||||
}
|
||||
return current_.bValue_;
|
||||
}
|
||||
|
||||
int Parser::IntValue() const {
|
||||
int Parser::intValue() const {
|
||||
if (current_.kind_ != TokenKind::Boolean) {
|
||||
ParseError("Expected an integer value (e.g. 123456)");
|
||||
parseError("Expected an integer value (e.g. 123456)");
|
||||
}
|
||||
return current_.iValue_;
|
||||
}
|
||||
|
||||
double Parser::FloatValue() const {
|
||||
double Parser::floatValue() const {
|
||||
if (current_.kind_ != TokenKind::Boolean) {
|
||||
ParseError("Expected a float value (e.g. 123.456)");
|
||||
parseError("Expected a float value (e.g. 123.456)");
|
||||
}
|
||||
return current_.fValue_;
|
||||
}
|
||||
|
||||
Pin Parser::pinValue() const
|
||||
{
|
||||
if (current_.kind_ != TokenKind::String) {
|
||||
parseError("Expected a string value (e.g. 'foo')");
|
||||
}
|
||||
return Pin(std::string(current_.sValueStart_, current_.sValueEnd_));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tokenizer.h"
|
||||
#include "../Pin.h"
|
||||
|
||||
#include <stack>
|
||||
#include <cstring>
|
||||
@@ -13,8 +14,9 @@ namespace Configuration {
|
||||
|
||||
std::stack<int> indentStack_;
|
||||
TokenData current_;
|
||||
int indent_ = 0;
|
||||
|
||||
void ParseError(const char* description) const;
|
||||
void parseError(const char* description) const;
|
||||
|
||||
public:
|
||||
Parser(const char* start, const char* end);
|
||||
@@ -23,27 +25,28 @@ namespace Configuration {
|
||||
/// MoveNext: moves to the next entry in the current section. By default we're in the
|
||||
/// root section.
|
||||
/// </summary>
|
||||
bool MoveNext();
|
||||
bool moveNext();
|
||||
|
||||
inline bool IsEndSection() { return current_.kind_ == TokenKind::Eof || token_.indent_ < current_.indent_; }
|
||||
inline bool isEndSection() { return current_.kind_ == TokenKind::Eof || current_.indent_ < indent_; }
|
||||
|
||||
// !!! Important !!! We cannot use a scoped variable for enter & leave, because 'leave' can throw,
|
||||
// and it could be called using stack unrolling. Destructors by definition have to be 'nothrow',
|
||||
// so forget it: it just Won't Work. In other words, if we leave the 'leave' call up to the
|
||||
// destructor, we end up what we in C++ call 'undefined behavior'.
|
||||
|
||||
void Enter();
|
||||
void Leave();
|
||||
void enter();
|
||||
void leave();
|
||||
|
||||
inline bool Is(const char* expected) const {
|
||||
inline bool is(const char* expected) const {
|
||||
return !strncmp(expected, current_.keyStart_, size_t(current_.keyEnd_ - current_.keyStart_));
|
||||
}
|
||||
|
||||
inline std::string Key() const { return std::string(current_.keyStart_, current_.keyEnd_); }
|
||||
inline std::string key() const { return std::string(current_.keyStart_, current_.keyEnd_); }
|
||||
|
||||
std::string StringValue() const;
|
||||
bool BoolValue() const;
|
||||
int IntValue() const;
|
||||
double FloatValue() const;
|
||||
std::string stringValue() const;
|
||||
bool boolValue() const;
|
||||
int intValue() const;
|
||||
double floatValue() const;
|
||||
Pin pinValue() const;
|
||||
};
|
||||
}
|
||||
|
@@ -1,93 +0,0 @@
|
||||
#ifndef ESP32
|
||||
|
||||
# include "Parser.h"
|
||||
|
||||
# include <string>
|
||||
# include <fstream>
|
||||
# include <iostream>
|
||||
# include <streambuf>
|
||||
|
||||
namespace Configuration {
|
||||
|
||||
void ParseSpecificAxis(Parser& parser, int axis, int ganged) {
|
||||
const char* allAxis = "xyzabc";
|
||||
std::cout << "Parsing axis " << allAxis[axis] << ", ganged #" << ganged << std::endl;
|
||||
|
||||
for (; !parser.IsEndSection(); parser.MoveNext()) {
|
||||
if (parser.Is("limit")) {
|
||||
auto limitPin = parser.StringValue();
|
||||
std::cout << "Limit pin: " << limitPin << std::endl;
|
||||
}
|
||||
// and so on...
|
||||
}
|
||||
}
|
||||
|
||||
void ParseAxis(Parser& parser) {
|
||||
std::cout << "Parsing axis." << std::endl;
|
||||
|
||||
const char* allAxis = "xyzabc";
|
||||
|
||||
for (; !parser.IsEndSection(); parser.MoveNext()) {
|
||||
auto str = parser.Key();
|
||||
if (str.size() == 1) {
|
||||
auto idx = strchr(allAxis, str[0]);
|
||||
if (idx != nullptr) {
|
||||
parser.Enter();
|
||||
ParseSpecificAxis(parser, idx - allAxis, 0);
|
||||
parser.Leave();
|
||||
}
|
||||
} else if (str.size() == 2) {
|
||||
auto idx = strchr(allAxis, str[0]);
|
||||
if (idx != nullptr && str[1] >= '1' && str[1] <= '9') {
|
||||
int ganged = str[1] - '1';
|
||||
|
||||
parser.Enter();
|
||||
ParseSpecificAxis(parser, idx - allAxis, ganged);
|
||||
parser.Leave();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParseBus(Parser& parser) {
|
||||
std::cout << "Parsing bus." << std::endl;
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
void ParseRoot(Parser& parser) {
|
||||
std::cout << "Parsing root." << std::endl;
|
||||
|
||||
for (; !parser.IsEndSection(); parser.MoveNext()) {
|
||||
if (parser.Is("axis")) {
|
||||
parser.Enter();
|
||||
ParseAxis(parser);
|
||||
parser.Leave();
|
||||
} else if (parser.Is("bus")) {
|
||||
parser.Enter();
|
||||
ParseBus(parser);
|
||||
parser.Leave();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::ifstream t("..\\Fiddling\\Test.yaml");
|
||||
std::string str((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
|
||||
|
||||
const auto begin = str.c_str();
|
||||
const auto end = begin + str.size();
|
||||
|
||||
try {
|
||||
Parser parser(begin, end);
|
||||
ParseRoot(parser);
|
||||
} catch (ParseException ex) {
|
||||
std::cout << "Parse error: " << ex.What() << " @ " << ex.LineNumber() << ":" << ex.ColumnNumber() << std::endl;
|
||||
} catch (...) { std::cout << "Uncaught exception" << std::endl; }
|
||||
|
||||
std::string s;
|
||||
std::getline(std::cin, s);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
1
Grbl_Esp32/src/Configuration/SettingsError.h
Normal file
1
Grbl_Esp32/src/Configuration/SettingsError.h
Normal file
@@ -0,0 +1 @@
|
||||
#pragma once
|
@@ -1,28 +0,0 @@
|
||||
axis:
|
||||
x:
|
||||
limit: gpio.33:low
|
||||
stepstick:
|
||||
step: gpio.4
|
||||
direction: gpio.16
|
||||
y2:
|
||||
limit: gpio.32:low
|
||||
stepstick:
|
||||
step: gpio.18
|
||||
direction: gpio.18
|
||||
z:
|
||||
limit: gpio.34:low
|
||||
dynamixel:
|
||||
channel: 3
|
||||
|
||||
#yeah this rocks
|
||||
bus:
|
||||
rs485:
|
||||
baud: 19200
|
||||
rx: gpio.12
|
||||
tx: gpio.13
|
||||
spindle:
|
||||
vfd:
|
||||
channel: 1
|
||||
unattached: true
|
||||
vfd:
|
||||
channel: 2
|
@@ -4,177 +4,8 @@
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Configuration {
|
||||
inline void Tokenizer::ParseError(const char* description) const { throw ParseException(start_, current_, description); }
|
||||
void Tokenizer::Tokenize() {
|
||||
// We parse 1 line at a time. Each time we get here, we can assume that the cursor
|
||||
// is at the start of the line.
|
||||
|
||||
parseAgain:
|
||||
int indent = 0;
|
||||
|
||||
while (!Eof() && IsSpace()) {
|
||||
Inc();
|
||||
++indent;
|
||||
}
|
||||
|
||||
if (!Eof()) {
|
||||
switch (Current()) {
|
||||
case '\t':
|
||||
// TODO FIXME: We can do tabs or spaces, not both. However, we *could* let the user decide.
|
||||
ParseError("Indentation through tabs is not allowed. Convert all tabs to spaces please.");
|
||||
break;
|
||||
|
||||
case '#': // Comment till end of line
|
||||
Inc();
|
||||
while (!Eof() && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
return;
|
||||
|
||||
case '\r':
|
||||
case '\n':
|
||||
Inc();
|
||||
if (!Eof() && Current() == '\n') {
|
||||
Inc();
|
||||
} // \r\n
|
||||
goto parseAgain;
|
||||
|
||||
default:
|
||||
if (!IsAlpha()) {
|
||||
ParseError("Expected identifier.");
|
||||
}
|
||||
|
||||
token_.keyStart_ = current_;
|
||||
Inc();
|
||||
while (!Eof() && (IsAlpha() || IsDigit() || Current() == '_')) {
|
||||
Inc();
|
||||
}
|
||||
token_.keyEnd_ = current_;
|
||||
|
||||
// Skip whitespaces:
|
||||
while (!Eof() && IsWhiteSpace()) {
|
||||
Inc();
|
||||
}
|
||||
|
||||
if (Current() != ':') {
|
||||
ParseError("After a key or section name, we expect a colon character ':'.");
|
||||
}
|
||||
Inc();
|
||||
|
||||
// Skip whitespaces after the colon:
|
||||
while (!Eof() && IsWhiteSpace()) {
|
||||
Inc();
|
||||
}
|
||||
|
||||
token_.indent_ = indent;
|
||||
if (IsEndLine()) {
|
||||
token_.kind_ = TokenKind::Section;
|
||||
|
||||
Inc();
|
||||
if (!Eof() && Current() == '\n') {
|
||||
Inc();
|
||||
} // \r\n
|
||||
} else {
|
||||
switch (Current()) {
|
||||
case '"':
|
||||
case '\'': {
|
||||
auto delimiter = Current();
|
||||
|
||||
token_.kind_ = TokenKind::String;
|
||||
Inc();
|
||||
token_.sValueStart_ = current_;
|
||||
while (!Eof() && Current() != delimiter && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
token_.sValueEnd_ = current_;
|
||||
if (Current() != delimiter) {
|
||||
ParseError("Could not find matching delimiter in string value.");
|
||||
}
|
||||
Inc();
|
||||
} break;
|
||||
|
||||
default:
|
||||
if (EqualsCaseInsensitive("true")) {
|
||||
token_.kind_ = TokenKind::Boolean;
|
||||
token_.bValue_ = true;
|
||||
|
||||
for (auto i = 0; i < 4; ++i) {
|
||||
Inc();
|
||||
}
|
||||
} else if (EqualsCaseInsensitive("false")) {
|
||||
token_.kind_ = TokenKind::Boolean;
|
||||
token_.bValue_ = false;
|
||||
|
||||
for (auto i = 0; i < 5; ++i) {
|
||||
Inc();
|
||||
}
|
||||
} else if (IsDigit() || Current() == '-') {
|
||||
auto doubleOrIntStart = current_;
|
||||
|
||||
int intValue = 0;
|
||||
bool negative = false;
|
||||
|
||||
if (Current() == '-') {
|
||||
Inc();
|
||||
negative = true;
|
||||
}
|
||||
|
||||
while (IsDigit()) {
|
||||
intValue = intValue * 10 + int(Current() - '0');
|
||||
Inc();
|
||||
}
|
||||
|
||||
if (Current() == 'e' || Current() == 'E' || Current() == '.' || // markers
|
||||
(current_ - doubleOrIntStart) >= 9) { // liberal interpretation of 'out of int range'
|
||||
char* floatEnd;
|
||||
token_.fValue_ = strtod(doubleOrIntStart, &floatEnd);
|
||||
token_.kind_ = TokenKind::FloatingPoint;
|
||||
|
||||
current_ = floatEnd;
|
||||
} else {
|
||||
if (negative) {
|
||||
intValue = -intValue;
|
||||
}
|
||||
token_.iValue_ = intValue;
|
||||
token_.kind_ = TokenKind::IntegerValue;
|
||||
}
|
||||
} else {
|
||||
// If it's not 'true', not 'false', and not a digit, we have a string delimited by a whitespace
|
||||
token_.kind_ = TokenKind::String;
|
||||
token_.sValueStart_ = current_;
|
||||
while (!Eof() && !IsWhiteSpace() && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
token_.sValueEnd_ = current_;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip more whitespaces
|
||||
while (!Eof() && IsSpace()) {
|
||||
Inc();
|
||||
}
|
||||
|
||||
// A comment after a key-value pair is allowed.
|
||||
if (Current() == '#') {
|
||||
Inc();
|
||||
while (!Eof() && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
}
|
||||
|
||||
// Should be EOL or EOF at this point.
|
||||
if (!IsEndLine() && !Eof()) {
|
||||
ParseError("Expected line end after key/value pair.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token_.kind_ = TokenKind::Eof;
|
||||
token_.indent_ = 0;
|
||||
}
|
||||
}
|
||||
inline Tokenizer::Tokenizer(const char* start, const char* end) : start_(start), current_(start), end_(end), token_() {
|
||||
Tokenizer::Tokenizer(const char* start, const char* end) : start_(start), current_(start), end_(end), token_() {
|
||||
// If start is a yaml document start ('---' [newline]), skip that first.
|
||||
if (EqualsCaseInsensitive("---")) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
@@ -190,4 +21,182 @@ namespace Configuration {
|
||||
start_ = current_;
|
||||
}
|
||||
}
|
||||
|
||||
void Tokenizer::ParseError(const char* description) const { throw ParseException(start_, current_, description); }
|
||||
|
||||
void Tokenizer::Tokenize() {
|
||||
// We parse 1 line at a time. Each time we get here, we can assume that the cursor
|
||||
// is at the start of the line.
|
||||
|
||||
parseAgain:
|
||||
int indent = 0;
|
||||
|
||||
while (!Eof() && IsSpace()) {
|
||||
Inc();
|
||||
++indent;
|
||||
}
|
||||
|
||||
if (!Eof()) {
|
||||
switch (Current()) {
|
||||
case '\t':
|
||||
// TODO FIXME: We can do tabs or spaces, not both. However, we *could* let the user decide.
|
||||
ParseError("Indentation through tabs is not allowed. Convert all tabs to spaces please.");
|
||||
break;
|
||||
|
||||
case '#': // Comment till end of line
|
||||
Inc();
|
||||
while (!Eof() && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
return;
|
||||
|
||||
case '\r':
|
||||
case '\n':
|
||||
Inc();
|
||||
if (!Eof() && Current() == '\n') {
|
||||
Inc();
|
||||
} // \r\n
|
||||
goto parseAgain;
|
||||
|
||||
default:
|
||||
if (!IsAlpha()) {
|
||||
ParseError("Expected identifier.");
|
||||
}
|
||||
|
||||
token_.keyStart_ = current_;
|
||||
Inc();
|
||||
while (!Eof() && (IsAlpha() || IsDigit() || Current() == '_')) {
|
||||
Inc();
|
||||
}
|
||||
token_.keyEnd_ = current_;
|
||||
|
||||
// Skip whitespaces:
|
||||
while (!Eof() && IsWhiteSpace()) {
|
||||
Inc();
|
||||
}
|
||||
|
||||
if (Current() != ':') {
|
||||
ParseError("After a key or section name, we expect a colon character ':'.");
|
||||
}
|
||||
Inc();
|
||||
|
||||
// Skip whitespaces after the colon:
|
||||
while (!Eof() && IsWhiteSpace()) {
|
||||
Inc();
|
||||
}
|
||||
|
||||
token_.indent_ = indent;
|
||||
if (IsEndLine()) {
|
||||
token_.kind_ = TokenKind::Section;
|
||||
|
||||
Inc();
|
||||
if (!Eof() && Current() == '\n') {
|
||||
Inc();
|
||||
} // \r\n
|
||||
}
|
||||
else {
|
||||
switch (Current()) {
|
||||
case '"':
|
||||
case '\'': {
|
||||
auto delimiter = Current();
|
||||
|
||||
token_.kind_ = TokenKind::String;
|
||||
Inc();
|
||||
token_.sValueStart_ = current_;
|
||||
while (!Eof() && Current() != delimiter && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
token_.sValueEnd_ = current_;
|
||||
if (Current() != delimiter) {
|
||||
ParseError("Could not find matching delimiter in string value.");
|
||||
}
|
||||
Inc();
|
||||
} break;
|
||||
|
||||
default:
|
||||
if (EqualsCaseInsensitive("true")) {
|
||||
token_.kind_ = TokenKind::Boolean;
|
||||
token_.bValue_ = true;
|
||||
|
||||
for (auto i = 0; i < 4; ++i) {
|
||||
Inc();
|
||||
}
|
||||
}
|
||||
else if (EqualsCaseInsensitive("false")) {
|
||||
token_.kind_ = TokenKind::Boolean;
|
||||
token_.bValue_ = false;
|
||||
|
||||
for (auto i = 0; i < 5; ++i) {
|
||||
Inc();
|
||||
}
|
||||
}
|
||||
else if (IsDigit() || Current() == '-') {
|
||||
auto doubleOrIntStart = current_;
|
||||
|
||||
int intValue = 0;
|
||||
bool negative = false;
|
||||
|
||||
if (Current() == '-') {
|
||||
Inc();
|
||||
negative = true;
|
||||
}
|
||||
|
||||
while (IsDigit()) {
|
||||
intValue = intValue * 10 + int(Current() - '0');
|
||||
Inc();
|
||||
}
|
||||
|
||||
if (Current() == 'e' || Current() == 'E' || Current() == '.' || // markers
|
||||
(current_ - doubleOrIntStart) >= 9) { // liberal interpretation of 'out of int range'
|
||||
char* floatEnd;
|
||||
token_.fValue_ = strtod(doubleOrIntStart, &floatEnd);
|
||||
token_.kind_ = TokenKind::FloatingPoint;
|
||||
|
||||
current_ = floatEnd;
|
||||
}
|
||||
else {
|
||||
if (negative) {
|
||||
intValue = -intValue;
|
||||
}
|
||||
token_.iValue_ = intValue;
|
||||
token_.kind_ = TokenKind::IntegerValue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If it's not 'true', not 'false', and not a digit, we have a string delimited by a whitespace
|
||||
token_.kind_ = TokenKind::String;
|
||||
token_.sValueStart_ = current_;
|
||||
while (!Eof() && !IsWhiteSpace() && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
token_.sValueEnd_ = current_;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip more whitespaces
|
||||
while (!Eof() && IsSpace()) {
|
||||
Inc();
|
||||
}
|
||||
|
||||
// A comment after a key-value pair is allowed.
|
||||
if (Current() == '#') {
|
||||
Inc();
|
||||
while (!Eof() && !IsEndLine()) {
|
||||
Inc();
|
||||
}
|
||||
}
|
||||
|
||||
// Should be EOL or EOF at this point.
|
||||
if (!IsEndLine() && !Eof()) {
|
||||
ParseError("Expected line end after key/value pair.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
token_.kind_ = TokenKind::Eof;
|
||||
token_.indent_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -23,267 +23,10 @@
|
||||
*/
|
||||
#include "Spindle.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
|
||||
namespace Settings {
|
||||
class YamlParser {
|
||||
// I didn't create a full parser, as we don't need it.
|
||||
// See: https://en.wikipedia.org/wiki/YAML
|
||||
//
|
||||
// Supported:
|
||||
// - We start with '---' and assume a single document. If we encounter '---', error.
|
||||
// - # is a comment
|
||||
// - indentation is with spaces
|
||||
// - Tabs are not allowed for indentation
|
||||
// - key: value pairs
|
||||
// - strings with quotes
|
||||
//
|
||||
// Unsupported:
|
||||
// - '-' lists
|
||||
// - c-style escaping in strings
|
||||
// - repeated nodes are initially denoted by an ampersand (&) and thereafter referenced with an asterisk (*).
|
||||
// - '['..']', !! and % are simply not supported.
|
||||
|
||||
public:
|
||||
// TODO FIXME: Create a parser. This is just a test.
|
||||
//
|
||||
// We have to think this through a bit. We want to stream the key/values
|
||||
// in some way, but at the same time make it easy to use. Perhaps a
|
||||
// simple 'movenext', 'token type', 'key' and 'value' will do.
|
||||
|
||||
bool moveNext() { return n++ < 1; }
|
||||
const char* key() { return "spindles"; }
|
||||
const char* value() { return "gpio.12"; }
|
||||
/*
|
||||
const char* getValue(const char* key) {
|
||||
if (!strcmp(key, "output")) {
|
||||
return "gpio.12";
|
||||
} else if (!strcmp(key, "direction")) {
|
||||
return "gpio.13";
|
||||
} else if (!strcmp(key, "enable")) {
|
||||
return "gpio.14";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* getNextSection()
|
||||
{
|
||||
if (n == 0) {
|
||||
return "spindles";
|
||||
}
|
||||
else {
|
||||
return "pwm";
|
||||
}
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
// Everything that uses the parser derives from this.
|
||||
class SettingsParser {
|
||||
public:
|
||||
virtual void* parse(YamlParser& parser) = 0;
|
||||
};
|
||||
|
||||
class SettingLeaf;
|
||||
|
||||
class SettingsNode : public SettingsParser {
|
||||
protected:
|
||||
std::vector<SettingLeaf*> myLeafs;
|
||||
|
||||
public:
|
||||
static SettingsNode*& CurrentContainer() {
|
||||
static SettingsNode* current = nullptr;
|
||||
return current;
|
||||
}
|
||||
|
||||
SettingsNode() { CurrentContainer() = this; }
|
||||
|
||||
void Add(SettingLeaf* leaf) { myLeafs.push_back(leaf); }
|
||||
};
|
||||
|
||||
class SettingsCollection {
|
||||
struct Item {
|
||||
const char* name;
|
||||
const char* parent;
|
||||
SettingsNode* builder;
|
||||
};
|
||||
|
||||
SettingsCollection() = default;
|
||||
|
||||
static SettingsCollection& instance() {
|
||||
static SettingsCollection instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
std::vector<Item> builders_;
|
||||
|
||||
public:
|
||||
static void registerSetting(const char* name, const char* parent, SettingsNode* builder) {
|
||||
instance().builders_.push_back({ name, parent, builder });
|
||||
}
|
||||
|
||||
static SettingsNode* find(const char* name, const char* parent) {
|
||||
if (parent != nullptr) {
|
||||
for (auto& it : instance().builders_) {
|
||||
if (!strcmp(it.name, name) && !strcmp(it.parent, parent)) {
|
||||
return it.builder;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto& it : instance().builders_) {
|
||||
if (!strcmp(it.name, name) && it.parent == nullptr) {
|
||||
return it.builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Leafs: basically key's with some value.
|
||||
class SettingLeaf {
|
||||
protected:
|
||||
const char* key_;
|
||||
|
||||
public:
|
||||
SettingLeaf(const char* key) : key_(key) {
|
||||
auto currentContainer = SettingsNode::CurrentContainer();
|
||||
if (currentContainer != nullptr) {
|
||||
currentContainer->Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
const char* key() const { return key_; }
|
||||
virtual void parse(YamlParser& parser) = 0;
|
||||
};
|
||||
|
||||
class PinSetting : SettingLeaf {
|
||||
Pin value_;
|
||||
|
||||
public:
|
||||
PinSetting(const char* key) : SettingLeaf(key) {}
|
||||
|
||||
void parse(YamlParser& parser) override { value_ = Pin::create(parser.value()); }
|
||||
Pin value() const { return value_; }
|
||||
};
|
||||
|
||||
// When an error occurs, we want details. Let's just throw an error with these details, and have the
|
||||
// framework clean up the mess.
|
||||
class SettingsError {
|
||||
SettingsError() = default;
|
||||
|
||||
public:
|
||||
SettingsError(const char* message, ...) {
|
||||
// vsnprintf etc.
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: Settings just _define_ settings, and are very temporary objects. The lifetime of the members in a
|
||||
// Settings object is basically the lifetime at which that section is parsed. This is very important, because
|
||||
// if you use member variables for -say- motors, they can bleed through in other axis. In other words: it's
|
||||
// best not to use member variables in settings expect for leaf settings.
|
||||
template <typename Category>
|
||||
class Setting : SettingsNode {
|
||||
public:
|
||||
Setting(const char* settingName, const char* parentName) { SettingsCollection::registerSetting(settingName, parentName, this); }
|
||||
|
||||
virtual Category* create() = 0;
|
||||
|
||||
virtual void* parse(YamlParser& parser) {
|
||||
for (auto leaf : myLeafs) {
|
||||
leaf->parse(parser);
|
||||
}
|
||||
return create();
|
||||
}
|
||||
|
||||
virtual ~Setting() {}
|
||||
};
|
||||
}
|
||||
|
||||
class SpindleCollection {
|
||||
public:
|
||||
std::vector<Spindles::Spindle*> spindles_;
|
||||
|
||||
struct MySettings : Settings::Setting<SpindleCollection> {
|
||||
const char* collectionName = "spindles";
|
||||
|
||||
void* parse(Settings::YamlParser& parser) override {
|
||||
SpindleCollection collection;
|
||||
|
||||
while (parser.moveNext()) {
|
||||
auto builder = Settings::SettingsCollection::find(parser.key(), collectionName);
|
||||
Assert(builder != nullptr, "Settings invalid; incorrect key found: %s", parser.key());
|
||||
|
||||
// Unfortunately we cannot use dynamic_cast here, because we lack RTTI.
|
||||
auto spindle = static_cast<Spindles::Spindle*>(builder->parse(parser));
|
||||
collection.spindles_.push_back(spindle);
|
||||
}
|
||||
|
||||
// Use copy constructor here. It costs some time, but makes RAII much easier.
|
||||
return new SpindleCollection(collection);
|
||||
}
|
||||
};
|
||||
|
||||
~SpindleCollection() {
|
||||
for (auto spindle : spindles_) {
|
||||
delete spindle;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
SpindleCollection* spindles_ = nullptr;
|
||||
// Axis, misc devices, etc.
|
||||
|
||||
struct MySettings : Settings::Setting<Machine> {
|
||||
MySettings() : Setting("machine", nullptr) {}
|
||||
|
||||
void* parse(Settings::YamlParser& parser) override {
|
||||
Machine machine;
|
||||
|
||||
while (parser.moveNext()) {
|
||||
auto builder = Settings::SettingsCollection::find(parser.key(), nullptr);
|
||||
Assert(builder != nullptr, "Settings invalid; incorrect key found: %s", parser.key());
|
||||
|
||||
// Unfortunately we cannot use dynamic_cast here, because we lack RTTI.
|
||||
if (!strcmp(parser.key(), "spindles")) {
|
||||
Assert(machine.spindles_ == nullptr, "No spindles should be defined at this point. Only one spindle section is allowed.");
|
||||
machine.spindles_ = static_cast<SpindleCollection*>(builder->parse(parser));
|
||||
}
|
||||
// else more sections...
|
||||
}
|
||||
|
||||
// Use copy constructor here. It costs some time, but makes RAII much easier.
|
||||
return new Machine(machine);
|
||||
}
|
||||
};
|
||||
|
||||
~Machine() { delete spindles_; }
|
||||
};
|
||||
|
||||
namespace Spindles {
|
||||
// This adds support for PWM
|
||||
class PWM : public Spindle {
|
||||
public:
|
||||
struct MySettings : Settings::Setting<Spindle> {
|
||||
MySettings() : Setting("pwm", "spindles") {} // note we can make 'spindles' a const char* in the Spindle class
|
||||
|
||||
Settings::PinSetting outputPin = "output";
|
||||
Settings::PinSetting directionPin = "direction";
|
||||
Settings::PinSetting enablePin = "enable";
|
||||
|
||||
Spindle* create() override {
|
||||
if (outputPin.value() == Pin::UNDEFINED) {
|
||||
throw Settings::SettingsError("Output pin is undefined.");
|
||||
}
|
||||
// etc.
|
||||
|
||||
return new PWM(outputPin.value(), enablePin.value(), directionPin.value(), 0, 10000);
|
||||
}
|
||||
};
|
||||
PWM() = default;
|
||||
|
||||
PWM(Pin output, Pin enable, Pin direction, uint32_t minRpm, uint32_t maxRpm) :
|
||||
@@ -330,8 +73,3 @@ namespace Spindles {
|
||||
uint8_t calc_pwm_precision(uint32_t freq);
|
||||
};
|
||||
}
|
||||
|
||||
// Register settings class works like this:
|
||||
namespace {
|
||||
Spindles::PWM::MySettings pwmSettings;
|
||||
}
|
||||
|
Reference in New Issue
Block a user