Commit 8efb0a30 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'experimental/iotwebconfig' into develop

Current state seems to be stable enough for develop
parents 34496cfe 6ea33a0e
Pipeline #5893 passed with stage
in 2 minutes and 22 seconds
/**
* IotWebConfTParameter.h -- IotWebConf is an ESP8266/ESP32
* non blocking WiFi/AP web configuration library for Arduino.
* https://github.com/prampec/IotWebConf
*
* Copyright (C) 2021 Balazs Kelemen <prampec+arduino@gmail.com>
* rovo89
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
#ifndef IotWebConfTParameter_h
#define IotWebConfTParameter_h
// TODO: This file is a mess. Help wanted to organize thing!
#include "IotWebConfParameter.h"
#include <Arduino.h>
#include <IPAddress.h>
#include <errno.h>
// At least in PlatformIO, strtoimax/strtoumax are defined, but not implemented.
#if 1
#define strtoimax strtoll
#define strtoumax strtoull
#endif
namespace iotwebconf
{
/**
* This class is to hide web related properties from the
* data manipulation.
*/
class ConfigItemBridge : public ConfigItem
{
public:
virtual void update(WebRequestWrapper* webRequestWrapper) override
{
if (webRequestWrapper->hasArg(this->getId()))
{
String newValue = webRequestWrapper->arg(this->getId());
this->update(newValue);
}
}
void debugTo(Stream* out) override
{
out->print("'");
out->print(this->getId());
out->print("' with value: '");
out->print(this->toString());
out->println("'");
}
protected:
ConfigItemBridge(const char* id) : ConfigItem(id) { }
virtual int getInputLength() { return 0; };
virtual bool update(String newValue, bool validateOnly = false) = 0;
virtual String toString() = 0;
};
///////////////////////////////////////////////////////////////////////////
/**
* DataType is the data related part of the parameter.
* It does not care about web and visualization, but takes care of the
* data validation and storing.
*/
template <typename ValueType, typename _DefaultValueType = ValueType>
class DataType : virtual public ConfigItemBridge
{
public:
using DefaultValueType = _DefaultValueType;
DataType(const char* id, DefaultValueType defaultValue) :
ConfigItemBridge(id),
_defaultValue(defaultValue)
{
}
/**
* value() can be used to get the value, but it can also
* be used set it like this: p.value() = newValue
*/
ValueType& value() { return this->_value; }
ValueType& operator*() { return this->_value; }
protected:
int getStorageSize() override
{
return sizeof(ValueType);
}
virtual bool update(String newValue, bool validateOnly = false) = 0;
bool validate(String newValue) { return update(newValue, true); }
virtual String toString() override { return String(this->_value); }
ValueType _value;
const DefaultValueType _defaultValue;
};
///////////////////////////////////////////////////////////////////////////
class StringDataType : public DataType<String>
{
public:
using DataType<String>::DataType;
protected:
virtual bool update(String newValue, bool validateOnly) override {
if (!validateOnly)
{
this->_value = newValue;
}
return true;
}
virtual String toString() override { return this->_value; }
};
///////////////////////////////////////////////////////////////////////////
template <size_t len>
class CharArrayDataType : public DataType<char[len], const char*>
{
public:
using DataType<char[len], const char*>::DataType;
CharArrayDataType(const char* id, const char* defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
DataType<char[len], const char*>::DataType(id, defaultValue) { };
virtual void applyDefaultValue() override
{
strncpy(this->_value, this->_defaultValue, len);
}
protected:
virtual bool update(String newValue, bool validateOnly) override
{
if (newValue.length() + 1 > len)
{
return false;
}
if (!validateOnly)
{
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
Serial.print(this->getId());
Serial.print(": ");
Serial.println(newValue);
#endif
strncpy(this->_value, newValue.c_str(), len);
}
return true;
}
void storeValue(std::function<void(
SerializationData* serializationData)> doStore) override
{
SerializationData serializationData;
serializationData.length = len;
serializationData.data = (byte*)this->_value;
doStore(&serializationData);
}
void loadValue(std::function<void(
SerializationData* serializationData)> doLoad) override
{
SerializationData serializationData;
serializationData.length = len;
serializationData.data = (byte*)this->_value;
doLoad(&serializationData);
}
virtual int getInputLength() override { return len; };
};
///////////////////////////////////////////////////////////////////////////
/**
* All non-complex types should be inherited from this base class.
*/
template <typename ValueType>
class PrimitiveDataType : public DataType<ValueType>
{
public:
using DataType<ValueType>::DataType;
PrimitiveDataType(const char* id, ValueType defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
DataType<ValueType>::DataType(id, defaultValue) { };
void setMax(ValueType val) { this->_max = val; this->_maxDefined = true; }
void setMin(ValueType val) { this->_min = val; this->_minDefined = true; }
virtual void applyDefaultValue() override
{
this->_value = this->_defaultValue;
}
protected:
virtual bool update(String newValue, bool validateOnly) override
{
errno = 0;
ValueType val = fromString(newValue);
if ((errno == ERANGE)
|| (this->_minDefined && (val < this->_min))
|| (this->_maxDefined && (val > this->_max)))
{
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
Serial.print(this->getId());
Serial.print(" value not accepted: ");
Serial.println(val);
#endif
return false;
}
if (!validateOnly)
{
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
Serial.print(this->getId());
Serial.print(": ");
Serial.println((ValueType)val);
#endif
this->_value = (ValueType) val;
}
return true;
}
void storeValue(std::function<void(
SerializationData* serializationData)> doStore) override
{
SerializationData serializationData;
serializationData.length = this->getStorageSize();
serializationData.data =
reinterpret_cast<byte*>(&this->_value);
doStore(&serializationData);
}
void loadValue(std::function<void(
SerializationData* serializationData)> doLoad) override
{
byte buf[this->getStorageSize()];
SerializationData serializationData;
serializationData.length = this->getStorageSize();
serializationData.data = buf;
doLoad(&serializationData);
ValueType* valuePointer = reinterpret_cast<ValueType*>(buf);
this->_value = *valuePointer;
}
virtual ValueType fromString(String stringValue) = 0;
ValueType getMax() { return this->_max; }
ValueType getMin() { return this->_min; }
ValueType isMaxDefined() { return this->_maxDefined; }
ValueType isMinDefined() { return this->_minDefined; }
private:
ValueType _min;
ValueType _max;
bool _minDefined = false;
bool _maxDefined = false;
};
///////////////////////////////////////////////////////////////////////////
template <typename ValueType, int base = 10>
class SignedIntDataType : public PrimitiveDataType<ValueType>
{
public:
SignedIntDataType(const char* id, ValueType defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
PrimitiveDataType<ValueType>::PrimitiveDataType(id, defaultValue) { };
protected:
virtual ValueType fromString(String stringValue)
{
return (ValueType)strtoimax(stringValue.c_str(), nullptr, base);
}
};
template <typename ValueType, int base = 10>
class UnsignedIntDataType : public PrimitiveDataType<ValueType>
{
public:
UnsignedIntDataType(const char* id, ValueType defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
PrimitiveDataType<ValueType>::PrimitiveDataType(id, defaultValue) { };
protected:
virtual ValueType fromString(String stringValue)
{
return (ValueType)strtoumax(stringValue.c_str(), nullptr, base);
}
};
class BoolDataType : public PrimitiveDataType<bool>
{
public:
BoolDataType(const char* id, bool defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
PrimitiveDataType<bool>::PrimitiveDataType(id, defaultValue) { };
protected:
virtual bool fromString(String stringValue)
{
return stringValue.c_str()[0] == 1;
}
};
class FloatDataType : public PrimitiveDataType<float>
{
public:
FloatDataType(const char* id, float defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
PrimitiveDataType<float>::PrimitiveDataType(id, defaultValue) { };
protected:
virtual float fromString(String stringValue)
{
return atof(stringValue.c_str());
}
};
class DoubleDataType : public PrimitiveDataType<double>
{
public:
DoubleDataType(const char* id, double defaultValue) :
ConfigItemBridge::ConfigItemBridge(id),
PrimitiveDataType<double>::PrimitiveDataType(id, defaultValue) { };
protected:
virtual double fromString(String stringValue)
{
return strtod(stringValue.c_str(), nullptr);
}
};
/////////////////////////////////////////////////////////////////////////
class IpDataType : public DataType<IPAddress>
{
using DataType<IPAddress>::DataType;
protected:
virtual bool update(String newValue, bool validateOnly) override
{
if (validateOnly)
{
IPAddress ip;
return ip.fromString(newValue);
}
else
{
return this->_value.fromString(newValue);
}
}
virtual String toString() override { return this->_value.toString(); }
};
///////////////////////////////////////////////////////////////////////////
/**
* Input parameter is the part of the parameter that is responsible
* for the appearance of the parameter in HTML.
*/
class InputParameter : virtual public ConfigItemBridge
{
public:
InputParameter(const char* id, const char* label) :
ConfigItemBridge::ConfigItemBridge(id),
label(label) { }
virtual void renderHtml(
bool dataArrived, WebRequestWrapper* webRequestWrapper) override
{
String content = this->renderHtml(
dataArrived,
webRequestWrapper->hasArg(this->getId()),
webRequestWrapper->arg(this->getId()));
webRequestWrapper->sendContent(content);
}
const char* label;
/**
* This variable is meant to store a value that is displayed in an empty
* (not filled) field.
*/
const char* placeholder = nullptr;
virtual void setPlaceholder(const char* placeholder) { this->placeholder = placeholder; }
/**
* Usually this variable is used when rendering the form input field
* so one can customize the rendered outcome of this particular item.
*/
const char* customHtml = nullptr;
/**
* Used when rendering the input field. Is is overridden by different
* implementations.
*/
virtual String getCustomHtml()
{
return String(customHtml == nullptr ? "" : customHtml);
}
const char* errorMessage = nullptr;
protected:
void clearErrorMessage() override
{
this->errorMessage = nullptr;
}
virtual String renderHtml(
bool dataArrived, bool hasValueFromPost, String valueFromPost)
{
String pitem = String(this->getHtmlTemplate());
pitem.replace("{b}", this->label);
pitem.replace("{t}", this->getInputType());
pitem.replace("{i}", this->getId());
pitem.replace(
"{p}", this->placeholder == nullptr ? "" : this->placeholder);
int length = this->getInputLength();
if (length > 0)
{
char parLength[11];
snprintf(parLength, 11, "%d", length);
String maxLength = String("maxlength=") + parLength;
pitem.replace("{l}", maxLength);
}
else
{
pitem.replace("{l}", "");
}
if (hasValueFromPost)
{
// -- Value from previous submit
pitem.replace("{v}", valueFromPost);
}
else
{
// -- Value from config
pitem.replace("{v}", this->toString());
}
pitem.replace("{c}", this->getCustomHtml());
pitem.replace(
"{s}",
this->errorMessage == nullptr ? "" : "de"); // Div style class.
pitem.replace(
"{e}",
this->errorMessage == nullptr ? "" : this->errorMessage);
return pitem;
}
/**
* One can override this method in case a specific HTML template is required
* for a parameter.
*/
virtual String getHtmlTemplate() { return FPSTR(IOTWEBCONF_HTML_FORM_PARAM); };
virtual const char* getInputType() = 0;
};
template <size_t len>
class TextTParameter : public CharArrayDataType<len>, public InputParameter
{
public:
using CharArrayDataType<len>::CharArrayDataType;
TextTParameter(const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
CharArrayDataType<len>::CharArrayDataType(id, defaultValue),
InputParameter::InputParameter(id, label) { }
protected:
virtual const char* getInputType() override { return "text"; }
};
class CheckboxTParameter : public BoolDataType, public InputParameter
{
public:
CheckboxTParameter(const char* id, const char* label, const bool defaultValue) :
ConfigItemBridge(id),
BoolDataType::BoolDataType(id, defaultValue),
InputParameter::InputParameter(id, label) { }
bool isChecked() { return this->value(); }
protected:
virtual const char* getInputType() override { return "checkbox"; }
virtual void update(WebRequestWrapper* webRequestWrapper) override
{
bool selected = false;
if (webRequestWrapper->hasArg(this->getId()))
{
String valueFromPost = webRequestWrapper->arg(this->getId());
selected = valueFromPost.equals("selected");
}
// this->update(String(selected ? "1" : "0"));
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
Serial.print(this->getId());
Serial.print(": ");
Serial.println(selected ? "selected" : "not selected");
#endif
this->_value = selected;
}
virtual String renderHtml(
bool dataArrived, bool hasValueFromPost, String valueFromPost) override
{
bool checkSelected = false;
if (dataArrived)
{
if (hasValueFromPost && valueFromPost.equals("selected"))
{
checkSelected = true;
}
}
else
{
if (this->isChecked())
{
checkSelected = true;
}
}
if (checkSelected)
{
this->customHtml = CheckboxTParameter::_checkedStr;
}
else
{
this->customHtml = nullptr;
}
return InputParameter::renderHtml(dataArrived, true, String("selected"));
}
private:
const char* _checkedStr = "checked='checked'";
};
template <size_t len>
class PasswordTParameter : public CharArrayDataType<len>, public InputParameter
{
public:
using CharArrayDataType<len>::CharArrayDataType;
PasswordTParameter(const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
CharArrayDataType<len>::CharArrayDataType(id, defaultValue),
InputParameter::InputParameter(id, label)
{
this->customHtml = _customHtmlPwd;
}
void debugTo(Stream* out)
{
out->print("'");
out->print(this->getId());
out->print("' with value: ");
#ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL
out->print("'");
out->print(this->_value);
out->println("'");
#else
out->println(F("<hidden>"));
#endif
}
virtual bool update(String newValue, bool validateOnly) override
{
if (newValue.length() + 1 > len)
{
return false;
}
if (validateOnly)
{
return true;
}
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
Serial.print(this->getId());
Serial.print(": ");
#endif
if (newValue != this->_value)
{
// -- Value was set.
strncpy(this->_value, newValue.c_str(), len);
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL
Serial.println(this->_value);
# else
Serial.println("<updated>");
# endif
#endif
}
else
{
#ifdef IOTWEBCONF_DEBUG_TO_SERIAL
Serial.println("<was not changed>");
#endif
}
return true;
}
protected:
virtual const char* getInputType() override { return "password"; }
virtual String renderHtml(
bool dataArrived, bool hasValueFromPost, String valueFromPost) override
{
return InputParameter::renderHtml(dataArrived, true, String(this->_value));
}
private:
const char* _customHtmlPwd = "ondblclick=\"pw(this.id)\"";
};
/**
* All non-complex type input parameters should be inherited from this
* base class.
*/
template <typename ValueType>
class PrimitiveInputParameter :
public InputParameter
{
public:
PrimitiveInputParameter(const char* id, const char* label) :
ConfigItemBridge::ConfigItemBridge(id),
InputParameter::InputParameter(id, label) { }
virtual String getCustomHtml() override
{
String modifiers = String(this->customHtml);
if (this->isMinDefined())
{
modifiers += " min='" ;
modifiers += this->getMin();
modifiers += "'";
}
if (this->isMaxDefined())
{
modifiers += " max='";
modifiers += this->getMax();
modifiers += "'";
}
if (this->step != 0)
{
modifiers += " step='";
modifiers += this->step;
modifiers += "'";
}
return modifiers;
}
ValueType step = 0;
void setStep(ValueType step) { this->step = step; }
virtual ValueType getMin() = 0;
virtual ValueType getMax() = 0;
virtual bool isMinDefined() = 0;
virtual bool isMaxDefined() = 0;
};
template <typename ValueType, int base = 10>
class IntTParameter :
public virtual SignedIntDataType<ValueType, base>,
public PrimitiveInputParameter<ValueType>
{
public:
IntTParameter(const char* id, const char* label, ValueType defaultValue) :
ConfigItemBridge(id),
SignedIntDataType<ValueType, base>::SignedIntDataType(id, defaultValue),
PrimitiveInputParameter<ValueType>::PrimitiveInputParameter(id, label) { }
// TODO: somehow organize these methods into common parent.
virtual ValueType getMin() override
{
return PrimitiveDataType<ValueType>::getMin();
}
virtual ValueType getMax() override
{
return PrimitiveDataType<ValueType>::getMax();
}
virtual bool isMinDefined() override
{
return PrimitiveDataType<ValueType>::isMinDefined();
}
virtual bool isMaxDefined() override
{
return PrimitiveDataType<ValueType>::isMaxDefined();
}
protected:
virtual const char* getInputType() override { return "number"; }
};
template <typename ValueType, int base = 10>
class UIntTParameter :
public virtual UnsignedIntDataType<ValueType, base>,
public PrimitiveInputParameter<ValueType>
{
public:
UIntTParameter(const char* id, const char* label, ValueType defaultValue) :
ConfigItemBridge(id),
UnsignedIntDataType<ValueType, base>::UnsignedIntDataType(id, defaultValue),
PrimitiveInputParameter<ValueType>::PrimitiveInputParameter(id, label) { }
// TODO: somehow organize these methods into common parent.
virtual ValueType getMin() override
{
return PrimitiveDataType<ValueType>::getMin();
}
virtual ValueType getMax() override
{
return PrimitiveDataType<ValueType>::getMax();
}
virtual bool isMinDefined() override
{
return PrimitiveDataType<ValueType>::isMinDefined();
}
virtual bool isMaxDefined() override
{
return PrimitiveDataType<ValueType>::isMaxDefined();
}
protected:
virtual const char* getInputType() override { return "number"; }
};
class FloatTParameter :
public FloatDataType,
public PrimitiveInputParameter<float>
{
public:
FloatTParameter(const char* id, const char* label, float defaultValue) :
ConfigItemBridge(id),
FloatDataType::FloatDataType(id, defaultValue),
PrimitiveInputParameter<float>::PrimitiveInputParameter(id, label) { }
virtual float getMin() override
{
return PrimitiveDataType<float>::getMin();
}
virtual float getMax() override
{
return PrimitiveDataType<float>::getMax();
}
virtual bool isMinDefined() override
{
return PrimitiveDataType<float>::isMinDefined();
}
virtual bool isMaxDefined() override
{
return PrimitiveDataType<float>::isMaxDefined();
}
protected:
virtual const char* getInputType() override { return "number"; }
};
/**
* Options parameter is a structure, that handles multiple values when redering
* the HTML representation.
*/
template <size_t len>
class OptionsTParameter : public TextTParameter<len>
{
public:
/**
* @optionValues - List of values to choose from with, where each value
* can have a maximal size of 'length'. Contains 'optionCount' items.
* @optionNames - List of names to render for the values, where each
* name can have a maximal size of 'nameLength'. Contains 'optionCount'
* items.
* @optionCount - Size of both 'optionValues' and 'optionNames' lists.
* @nameLength - Size of any item in optionNames list.
* (See TextParameter for arguments!)
*/
OptionsTParameter(
const char* id, const char* label, const char* defaultValue,
const char* optionValues, const char* optionNames,
size_t optionCount, size_t nameLength) :
ConfigItemBridge(id),
TextTParameter<len>(id, label, defaultValue)
{
this->_optionValues = optionValues;
this->_optionNames = optionNames;
this->_optionCount = optionCount;
this->_nameLength = nameLength;
}
// TODO: make these protected
void setOptionValues(const char* optionValues) { this->_optionValues = optionValues; }
void setOptionNames(const char* optionNames) { this->_optionNames = optionNames; }
void setOptionCount(size_t optionCount) { this->_optionCount = optionCount; }
void setNameLength(size_t nameLength) { this->_nameLength = nameLength; }
protected:
OptionsTParameter(
const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
TextTParameter<len>(id, label, defaultValue)
{
}
const char* _optionValues;
const char* _optionNames;
size_t _optionCount;
size_t _nameLength;
};
///////////////////////////////////////////////////////////////////////////////
/**
* Select parameter is an option parameter, that rendered as HTML SELECT.
* Basically it is a dropdown combobox.
*/
template <size_t len>
class SelectTParameter : public OptionsTParameter<len>
{
public:
/**
* Create a select parameter for the config portal.
*
* (See OptionsParameter for arguments!)
*/
SelectTParameter(
const char* id, const char* label, const char* defaultValue,
const char* optionValues, const char* optionNames,
size_t optionCount, size_t nameLength) :
ConfigItemBridge(id),
OptionsTParameter<len>(
id, label, defaultValue, optionValues, optionNames, optionCount, nameLength)
{ }
// TODO: make this protected
SelectTParameter(
const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
OptionsTParameter<len>(id, label, defaultValue) { }
protected:
// Overrides
virtual String renderHtml(
bool dataArrived, bool hasValueFromPost, String valueFromPost) override
{
String options = "";
for (size_t i=0; i<this->_optionCount; i++)
{
const char *optionValue = (this->_optionValues + (i*len) );
const char *optionName = (this->_optionNames + (i*this->_nameLength) );
String oitem = FPSTR(IOTWEBCONF_HTML_FORM_OPTION);
oitem.replace("{v}", optionValue);
// if (sizeof(this->_optionNames) > i)
{
oitem.replace("{n}", optionName);
}
// else
// {
// oitem.replace("{n}", "?");
// }
if ((hasValueFromPost && (valueFromPost == optionValue)) ||
(strncmp(this->value(), optionValue, len) == 0))
{
// -- Value from previous submit
oitem.replace("{s}", " selected");
}
else
{
// -- Value from config
oitem.replace("{s}", "");
}
options += oitem;
}
String pitem = FPSTR(IOTWEBCONF_HTML_FORM_SELECT_PARAM);
pitem.replace("{b}", this->label);
pitem.replace("{i}", this->getId());
pitem.replace(
"{c}", this->customHtml == nullptr ? "" : this->customHtml);
pitem.replace(
"{s}",
this->errorMessage == nullptr ? "" : "de"); // Div style class.
pitem.replace(
"{e}",
this->errorMessage == nullptr ? "" : this->errorMessage);
pitem.replace("{o}", options);
return pitem;
}
private:
};
///////////////////////////////////////////////////////////////////////////////
/**
* Color chooser.
*/
class ColorTParameter : public CharArrayDataType<8>, public InputParameter
{
public:
using CharArrayDataType<8>::CharArrayDataType;
ColorTParameter(const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
CharArrayDataType<8>::CharArrayDataType(id, defaultValue),
InputParameter::InputParameter(id, label) { }
protected:
virtual const char* getInputType() override { return "color"; }
};
///////////////////////////////////////////////////////////////////////////////
/**
* Date chooser.
*/
class DateTParameter : public CharArrayDataType<11>, public InputParameter
{
public:
using CharArrayDataType<11>::CharArrayDataType;
DateTParameter(const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
CharArrayDataType<11>::CharArrayDataType(id, defaultValue),
InputParameter::InputParameter(id, label) { }
protected:
virtual const char* getInputType() override { return "date"; }
};
///////////////////////////////////////////////////////////////////////////////
/**
* Time chooser.
*/
class TimeTParameter : public CharArrayDataType<6>, public InputParameter
{
public:
using CharArrayDataType<6>::CharArrayDataType;
TimeTParameter(const char* id, const char* label, const char* defaultValue) :
ConfigItemBridge(id),
CharArrayDataType<6>::CharArrayDataType(id, defaultValue),
InputParameter::InputParameter(id, label) { }
protected:
virtual const char* getInputType() override { return "time"; }
};
} // end namespace
#include "IotWebConfTParameterBuilder.h"
#endif
/**
* IotWebConfTParameter.h -- IotWebConf is an ESP8266/ESP32
* non blocking WiFi/AP web configuration library for Arduino.
* https://github.com/prampec/IotWebConf
*
* Copyright (C) 2021 Balazs Kelemen <prampec+arduino@gmail.com>
* rovo89
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
#ifndef IotWebConfTParameterBuilder_h
#define IotWebConfTParameterBuilder_h
#include "IotWebConfTParameter.h"
namespace iotwebconf
{
template <typename ParamType> class Builder;
template <typename ParamType>
class AbstractBuilder
{
public:
AbstractBuilder(const char* id) : _id(id) { };
virtual ParamType build() const
{
ParamType instance = std::move(
ParamType(this->_id, this->_label, this->_defaultValue));
this->apply(&instance);
return instance;
}
Builder<ParamType>& label(const char* label)
{ this->_label = label; return static_cast<Builder<ParamType>&>(*this); }
Builder<ParamType>& defaultValue(typename ParamType::DefaultValueType defaultValue)
{ this->_defaultValue = defaultValue; return static_cast<Builder<ParamType>&>(*this); }
protected:
virtual ParamType* apply(ParamType* instance) const
{
return instance;
}
const char* _label;
const char* _id;
typename ParamType::DefaultValueType _defaultValue;
};
template <typename ParamType>
class Builder : public AbstractBuilder<ParamType>
{
public:
Builder(const char* id) : AbstractBuilder<ParamType>(id) { };
};
///////////////////////////////////////////////////////////////////////////
template <typename ValueType, typename ParamType>
class PrimitiveBuilder :
public AbstractBuilder<ParamType>
{
public:
PrimitiveBuilder<ValueType, ParamType>(const char* id) :
AbstractBuilder<ParamType>(id) { };
Builder<ParamType>& min(ValueType min) { this->_minDefined = true; this->_min = min; return static_cast<Builder<ParamType>&>(*this); }
Builder<ParamType>& max(ValueType max) { this->_maxDefined = true; this->_max = max; return static_cast<Builder<ParamType>&>(*this); }
Builder<ParamType>& step(ValueType step) { this->_step = step; return static_cast<Builder<ParamType>&>(*this); }
Builder<ParamType>& placeholder(const char* placeholder) { this->_placeholder = placeholder; return static_cast<Builder<ParamType>&>(*this); }
protected:
virtual ParamType* apply(
ParamType* instance) const override
{
if (this->_minDefined)
{
instance->setMin(this->_min);
}
if (this->_maxDefined)
{
instance->setMax(this->_max);
}
instance->setStep(this->_step);
instance->setPlaceholder(this->_placeholder);
return instance;
}
bool _minDefined = false;
bool _maxDefined = false;
ValueType _min;
ValueType _max;
ValueType _step = 0;
const char* _placeholder = nullptr;
};
template <typename ValueType, int base>
class Builder<IntTParameter<ValueType, base>> :
public PrimitiveBuilder<ValueType, IntTParameter<ValueType, base>>
{
public:
Builder<IntTParameter<ValueType, base>>(const char* id) :
PrimitiveBuilder<ValueType, IntTParameter<ValueType, base>>(id) { };
};
template <typename ValueType, int base>
class Builder<UIntTParameter<ValueType, base>> :
public PrimitiveBuilder<ValueType, UIntTParameter<ValueType, base>>
{
public:
Builder<UIntTParameter<ValueType, base>>(const char* id) :
PrimitiveBuilder<ValueType, UIntTParameter<ValueType, base>>(id) { };
};
template <>
class Builder<FloatTParameter> :
public PrimitiveBuilder<float, FloatTParameter>
{
public:
Builder<FloatTParameter>(const char* id) :
PrimitiveBuilder<float, FloatTParameter>(id) { };
};
template <size_t len>
class Builder<SelectTParameter<len>> :
public AbstractBuilder<SelectTParameter<len>>
{
public:
Builder<SelectTParameter<len>>(const char* id) :
AbstractBuilder<SelectTParameter<len>>(id) { };
virtual SelectTParameter<len> build() const override
{
return SelectTParameter<len>(
this->_id, this->_label, this->_defaultValue,
this->_optionValues, this->_optionNames,
this->_optionCount, this->_nameLength);
}
Builder<SelectTParameter<len>>& optionValues(const char* optionValues)
{ this->_optionValues = optionValues; return *this; }
Builder<SelectTParameter<len>>& optionNames(const char* optionNames)
{ this->_optionNames = optionNames; return *this; }
Builder<SelectTParameter<len>>& optionCount(size_t optionCount)
{ this->_optionCount = optionCount; return *this; }
Builder<SelectTParameter<len>>& nameLength(size_t nameLength)
{ this->_nameLength = nameLength; return *this; }
protected:
virtual SelectTParameter<len>* apply(
SelectTParameter<len>* instance) const override
{
instance->setOptionValues(this->_optionValues);
instance->setOptionNames(this->_optionNames);
instance->setOptionCount(this->_optionCount);
instance->setNameLength(this->_nameLength);
return instance;
}
private:
const char* _optionValues;
const char* _optionNames;
size_t _optionCount;
size_t _nameLength;
};
} // End namespace
#endif
/**
* IotWebConfUsing.h -- IotWebConf is an ESP8266/ESP32
* non blocking WiFi/AP web configuration library for Arduino.
* https://github.com/prampec/IotWebConf
*
* Copyright (C) 2020 Balazs Kelemen <prampec+arduino@gmail.com>
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
#ifndef IotWebConfUsing_h
#define IotWebConfUsing_h
// This "using" lines are just aliases, and should avoided.
using IotWebConfParameterGroup = iotwebconf::ParameterGroup;
using IotWebConfTextParameter = iotwebconf::TextParameter;
using IotWebConfPasswordParameter = iotwebconf::PasswordParameter;
using IotWebConfNumberParameter = iotwebconf::NumberParameter;
using IotWebConfCheckboxParameter = iotwebconf::CheckboxParameter;
using IotWebConfSelectParameter = iotwebconf::SelectParameter;
#endif
\ No newline at end of file
/**
* IotWebConfWebServerWrapper.h -- IotWebConf is an ESP8266/ESP32
* non blocking WiFi/AP web configuration library for Arduino.
* https://github.com/prampec/IotWebConf
*
* Copyright (C) 2020 Balazs Kelemen <prampec+arduino@gmail.com>
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
#ifndef WebServerWrapper_h
#define WebServerWrapper_h
#include <Arduino.h>
#include <IPAddress.h>
namespace iotwebconf
{
class WebRequestWrapper
{
public:
virtual const String hostHeader() const;
virtual IPAddress localIP();
virtual uint16_t localPort();
virtual const String uri() const;
virtual bool authenticate(const char * username, const char * password);
virtual void requestAuthentication();
virtual bool hasArg(const String& name);
virtual String arg(const String name);
virtual void sendHeader(const String& name, const String& value, bool first = false);
virtual void setContentLength(const size_t contentLength);
virtual void send(int code, const char* content_type = nullptr, const String& content = String(""));
virtual void sendContent(const String& content);
virtual void stop();
};
class WebServerWrapper
{
public:
virtual void handleClient();
virtual void begin();
};
} // end namespace
#endif
\ No newline at end of file
#ifndef CustomAmpelIotWebConfSettings_h
#define CustomAmpelIotWebConfSettings_h
/***************************************/
/**** CUSTOM AMPEL CODE ****************/
// Disable DEBUG, in order to save some space, at least on ESP8266
#if defined(ESP8266)
#define IOTWEBCONF_DEBUG_DISABLED
#endif
#endif
\ No newline at end of file
...@@ -149,20 +149,6 @@ int NTPClient::getSeconds() const { ...@@ -149,20 +149,6 @@ int NTPClient::getSeconds() const {
return (this->getEpochTime() % 60); return (this->getEpochTime() % 60);
} }
String NTPClient::getFormattedTime() const {
unsigned long rawTime = this->getEpochTime();
unsigned long hours = (rawTime % 86400L) / 3600;
String hoursStr = hours < 10 ? "0" + String(hours) : String(hours);
unsigned long minutes = (rawTime % 3600) / 60;
String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes);
unsigned long seconds = rawTime % 60;
String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds);
return hoursStr + ":" + minuteStr + ":" + secondStr;
}
void NTPClient::end() { void NTPClient::end() {
this->_udp->stop(); this->_udp->stop();
......
...@@ -20,7 +20,7 @@ class NTPClient { ...@@ -20,7 +20,7 @@ class NTPClient {
unsigned long _updateInterval = 60000; // In ms unsigned long _updateInterval = 60000; // In ms
unsigned long _currentEpoc = 0; // In s unsigned long _currentEpoc = 0; // In ms
unsigned long _lastUpdate = 0; // In ms unsigned long _lastUpdate = 0; // In ms
byte _packetBuffer[NTP_PACKET_SIZE]; byte _packetBuffer[NTP_PACKET_SIZE];
...@@ -97,11 +97,6 @@ class NTPClient { ...@@ -97,11 +97,6 @@ class NTPClient {
*/ */
void setUpdateInterval(unsigned long updateInterval); void setUpdateInterval(unsigned long updateInterval);
/**
* @return time formatted like `hh:mm:ss`
*/
String getFormattedTime() const;
/** /**
* @return time in seconds since Jan. 1, 1970 * @return time in seconds since Jan. 1, 1970
*/ */
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
# define esp_get_heap_fragmentation() ESP.getHeapFragmentation() # define esp_get_heap_fragmentation() ESP.getHeapFragmentation()
#elif defined(ESP32) #elif defined(ESP32)
# define esp_get_max_free_block_size() ESP.getMaxAllocHeap() //largest block of heap that can be allocated. # define esp_get_max_free_block_size() ESP.getMaxAllocHeap() //largest block of heap that can be allocated.
# define esp_get_heap_fragmentation() "?" // apparently not available for ESP32 # define esp_get_heap_fragmentation() -1 // apparently not available for ESP32
#endif #endif
namespace util { namespace util {
...@@ -26,7 +26,7 @@ class Ampel { ...@@ -26,7 +26,7 @@ class Ampel {
private: private:
static void showFreeSpace(); static void showFreeSpace();
public: public:
const char *version = "v0.2.3-DEV"; // Update manually after significant changes. const char *version = "webconf-DEV"; // Update manually after significant changes.
const char *board; const char *board;
const char *sensorId; const char *sensorId;
const char *macAddress; const char *macAddress;
......
#include "web_config.h"
#define STRING_LEN 33 // Should be enough for ip, addresses, passwords...
#include "config.h"
#ifndef AMPEL_PASSWORD
# error Missing config.h file. Please copy config.public.h to config.h.
# warning config.h has a new structure (e.g. AMPEL_WIFI should be set to true or false)
#endif
#include "util.h"
#include "sensor_console.h"
#include "src/lib/IotWebConf/src/IotWebConf.h"
#include "src/lib/IotWebConf/src/IotWebConfTParameter.h"
#include "src/lib/IotWebConf/src/IotWebConfOptionalGroup.h"
//TODO: Convert all strings to F-strings
namespace web_config {
#if defined(ESP8266)
ESP8266WebServer http(80); // Create a webserver object that listens for HTTP request on port 80
#elif defined(ESP32)
WebServer http(80);
#endif
DNSServer dnsServer; //TODO: Check if needed
IotWebConf *iotWebConf;
const char config_version[IOTWEBCONF_CONFIG_VERSION_LENGTH] = "a10"; // -- Configuration specific key. The value should be modified if config structure was changed.
using namespace iotwebconf;
/**
* WiFi params
*/
CheckboxTParameter ampelWifiParam =
Builder<CheckboxTParameter>("WiFi").label("WiFi?").defaultValue(AMPEL_WIFI).build();
IntTParameter<uint16_t> wifiTimeoutParam =
Builder<IntTParameter<uint16_t>>("wifi_timeout").label("WiFi timeout").defaultValue(WIFI_TIMEOUT).min(0).placeholder(
"[s]").build();
//TODO: Distribute to corresponding classes, possibly with callbacks?
//TODO: Chainedparameters?
/**
* CO2 sensor
*/
ParameterGroup co2Params = ParameterGroup("co2", "CO2 Sensor");
IntTParameter<uint16_t> timestepParam =
Builder<IntTParameter<uint16_t>>("timestep").label("Measurement timestep").defaultValue(MEASUREMENT_TIMESTEP).min(
2).max(1800).placeholder("[s]").build();
FloatTParameter temperatureOffsetParam =
Builder<FloatTParameter>("temp_offset").label("Temperature offset").defaultValue(TEMPERATURE_OFFSET).placeholder(
"[K]").step(0.1).build();
IntTParameter<uint16_t> altitudeParam = Builder<IntTParameter<uint16_t>>("altitude").label("Altitude").defaultValue(
ALTITUDE_ABOVE_SEA_LEVEL).min(0).step(1).placeholder("[m]").build();
IntTParameter<uint16_t> atmosphericCO2Param = Builder<IntTParameter<uint16_t>>("atmospheric_co2").label(
"Atmospheric CO2 concentration").defaultValue(
ATMOSPHERIC_CO2_CONCENTRATION).min(400).max(2000).step(1).placeholder("ppm").build();
CheckboxTParameter autoCalibrateParam = Builder<CheckboxTParameter>("asc").label("Auto-calibration?").defaultValue(
AUTO_CALIBRATE_SENSOR).build();
/**
* LED
*/
//NOTE: Could also be optional : for LED 0 / 1
ParameterGroup ledParams = ParameterGroup("LED", "LED");
IntTParameter<uint8_t> maxBrightnessParam =
Builder<IntTParameter<uint8_t>>("max_brightness").label("Max Brightness").defaultValue(MAX_BRIGHTNESS).min(0).max(
255).build();
IntTParameter<uint8_t> minBrightnessParam =
Builder<IntTParameter<uint8_t>>("min_brightness").label("Min Brightness").defaultValue(MIN_BRIGHTNESS).min(0).max(
255).build();
IntTParameter<uint16_t> ledCountParam = Builder<IntTParameter<uint16_t>>("led_count").label("LED ring").defaultValue(
LED_COUNT).min(12).max(16).step(4).build();
/**
* CSV
*/
OptionalParameterGroup csvParams = OptionalParameterGroup("CSV", "CSV", AMPEL_CSV);
IntTParameter<uint16_t> csvIntervalParam =
Builder<IntTParameter<uint16_t>>("csv_interval").label("CSV interval").defaultValue(CSV_INTERVAL).min(0).step(1).placeholder(
"[s]").build();
/**
* MQTT
*/
OptionalParameterGroup mqttParams = OptionalParameterGroup("MQTT", "MQTT", AMPEL_MQTT);
IntTParameter<uint16_t> mqttIntervalParam =
Builder<IntTParameter<uint16_t>>("mqtt_interval").label("MQTT interval").defaultValue(MQTT_SENDING_INTERVAL).min(
0).step(1).defaultValue(300).placeholder("[s]").build();
CheckboxTParameter mqttEncryptionParam =
Builder<CheckboxTParameter>("mqtt_encryption").label("Encrypt MQTT?").defaultValue(MQTT_ENCRYPTED).build();
CheckboxTParameter mqttCommandsParam =
Builder<CheckboxTParameter>("mqtt_commands").label("Allow MQTT commands?").defaultValue(ALLOW_MQTT_COMMANDS).build();
TextTParameter<STRING_LEN> mqttServerParam =
Builder<TextTParameter<STRING_LEN>>("mqtt_server").label("MQTT Server").defaultValue(MQTT_SERVER).build();
IntTParameter<uint16_t> mqttPortParam = Builder<IntTParameter<uint16_t>>("mqtt_port").label("MQTT Port").defaultValue(
MQTT_PORT).build();
TextTParameter<STRING_LEN> mqttUserParam =
Builder<TextTParameter<STRING_LEN>>("mqtt_user").label("MQTT User").defaultValue(MQTT_USER).build();
PasswordTParameter<STRING_LEN> mqttPasswordParam = Builder<PasswordTParameter<STRING_LEN>>("mqtt_password").label(
"MQTT password").defaultValue(MQTT_PASSWORD).build();
/**
* NTP Time
*/
ParameterGroup timeParams = ParameterGroup("NTP", "Time server");
TextTParameter<STRING_LEN> ntpServerParam =
Builder<TextTParameter<STRING_LEN>>("ntp_server").label("NTP Server").defaultValue(NTP_SERVER).build();
IntTParameter<int16_t> timeOffsetParam = Builder<IntTParameter<int16_t>>("timezone").label("Timezone").defaultValue(
UTC_OFFSET).min(-23).max(23).step(1).placeholder("[h]").build();
CheckboxTParameter dstParam = Builder<CheckboxTParameter>("dst").label("Daylight Saving Time?").defaultValue(
DAYLIGHT_SAVING_TIME).build();
/**
* LoRaWAN
*/
#if defined(ESP32)
OptionalParameterGroup loraParams = OptionalParameterGroup("LoRaWan", "LoRaWan", AMPEL_LORAWAN);
IntTParameter<uint16_t> loraIntervalParam =
Builder<IntTParameter<uint16_t>>("lora_interval").label("LoRa interval").defaultValue(LORAWAN_SENDING_INTERVAL).min(
0).step(1).defaultValue(300).placeholder("[s]").build();
TextTParameter<17> deviceEUIParam = Builder<TextTParameter<17>>("device_eui").label("Device EUI (MSB)").defaultValue(
LORAWAN_DEVICE_EUI).build();
TextTParameter<17> appEUIParam = Builder<TextTParameter<17>>("app_eui").label("App EUI (MSB)").defaultValue(
LORAWAN_APPLICATION_EUI).build();
PasswordTParameter<33> appKeyParam = Builder<PasswordTParameter<33>>("app_key").label("App key (MSB)").defaultValue(
LORAWAN_APPLICATION_KEY).build();
#endif
OptionalGroupHtmlFormatProvider optionalGroupHtmlFormatProvider;
void update() {
iotWebConf->doLoop(); // Listen for HTTP requests from clients
}
void setWifiConnectionCallback(void (*success_function)()) {
iotWebConf->setWifiConnectionCallback(success_function);
}
void setWifiFailCallback(void (*fail_function)()) {
std::function<WifiAuthInfo* ()> fail_and_return_null = [fail_function]() {
fail_function();
iotWebConf->forceApMode(true); // Will stay in AP.
return nullptr;
};
iotWebConf->setWifiConnectionFailedHandler(fail_and_return_null);
}
void defineStructure() {
iotWebConf->setHtmlFormatProvider(&optionalGroupHtmlFormatProvider);
iotWebConf->addSystemParameter(&ampelWifiParam);
iotWebConf->addSystemParameter(&wifiTimeoutParam);
iotWebConf->getThingNameParameter()->label = "Ampel name";
iotWebConf->getApPasswordParameter()->label = "Ampel password (user : 'admin')";
iotWebConf->getApPasswordParameter()->defaultValue = AMPEL_PASSWORD;
iotWebConf->getWifiSsidParameter()->defaultValue = WIFI_SSID;
iotWebConf->getWifiPasswordParameter()->defaultValue = WIFI_PASSWORD;
co2Params.addItem(&timestepParam);
co2Params.addItem(&temperatureOffsetParam);
co2Params.addItem(&altitudeParam);
co2Params.addItem(&atmosphericCO2Param);
co2Params.addItem(&autoCalibrateParam);
ledParams.addItem(&minBrightnessParam);
ledParams.addItem(&maxBrightnessParam);
ledParams.addItem(&ledCountParam);
timeParams.addItem(&ntpServerParam);
timeParams.addItem(&timeOffsetParam);
timeParams.addItem(&dstParam);
csvParams.addItem(&csvIntervalParam);
mqttParams.addItem(&mqttIntervalParam);
mqttParams.addItem(&mqttServerParam);
mqttParams.addItem(&mqttPortParam);
mqttParams.addItem(&mqttUserParam);
mqttParams.addItem(&mqttPasswordParam);
mqttParams.addItem(&mqttEncryptionParam);
mqttParams.addItem(&mqttCommandsParam);
#if defined(ESP32)
loraParams.addItem(&loraIntervalParam);
loraParams.addItem(&deviceEUIParam);
loraParams.addItem(&appEUIParam);
loraParams.addItem(&appKeyParam);
#endif
iotWebConf->addParameterGroup(&co2Params);
iotWebConf->addParameterGroup(&ledParams);
iotWebConf->addParameterGroup(&timeParams);
iotWebConf->addParameterGroup(&csvParams);
iotWebConf->addParameterGroup(&mqttParams);
#if defined(ESP32)
iotWebConf->addParameterGroup(&loraParams);
#endif
}
void defineCommands() {
sensor_console::defineCommand("save_config", config::save, F("(Saves the config to EEPROM)"));
sensor_console::defineCommand("reset_config", []() {
Serial.println(F("Resetting config..."));
iotWebConf->getRootParameterGroup()->applyDefaultValue();
iotWebConf->saveConfig();
Serial.println(F("Done!"));
}, F("(Resets the complete IotWeb config)"));
sensor_console::defineStringCommand("ssid", [](char *ssid) {
Serial.print(F("Setting WiFi ssid to "));
Serial.println(ssid);
strlcpy(iotWebConf->getWifiSsidParameter()->valueBuffer, ssid, iotWebConf->getWifiSsidParameter()->getLength());
iotWebConf->saveConfig();
}, F("name (Sets SSID to name)"));
sensor_console::defineStringCommand("pwd", [](char *ssid) {
Serial.print(F("Setting WiFi password to "));
Serial.println(ssid);
strlcpy(iotWebConf->getWifiPasswordParameter()->valueBuffer, ssid, iotWebConf->getWifiPasswordParameter()->getLength());
iotWebConf->saveConfig();
}, F("abc (Sets WiFi password to abc)"));
sensor_console::defineIntCommand("wifi", [](int32_t onOff) {
config::is_wifi_on = onOff;
iotWebConf->saveConfig();
Serial.print(F("WiFi - "));
Serial.println(onOff ? F("On!") : F("Off!"));
}, F("0/1 (Turns Wifi on/off)"));
}
void initialize() {
iotWebConf = new IotWebConf(strlen(AMPEL_NAME) == 0 ? ampel.sensorId : AMPEL_NAME, &dnsServer, &http, "",
config_version);
defineStructure();
defineCommands();
iotWebConf->loadConfig();
if (!config::is_wifi_on) {
Serial.println(F("Wifi is off : no access point or connection. Use 'wifi 1' to turn on again!"));
return;
}
//NOTE: Can only work if wifi is on.
sensor_console::defineIntCommand("ap", [](int onOff) {
//TODO: Add On-board LED effects to show the changes?
if (onOff) {
Serial.print(F("Enable "));
} else {
Serial.print(F("Disable "));
}
Serial.println(F("AP mode!"));
iotWebConf->forceApMode(onOff);
}, F("0/1 (Enables/disables access point)"));
iotWebConf->setWifiConnectionTimeoutMs(1000UL * config::wifi_timeout);
iotWebConf->skipApStartup();
iotWebConf->init();
http.on("/config", [] {
iotWebConf->handleConfig();
});
http.onNotFound([]() {
iotWebConf->handleNotFound();
});
}
}
/***
* Define all the corresponding config values as reference, so that they can be updated.
*/
namespace config {
char* ampel_name() {
return web_config::iotWebConf->getThingName();
}
char* selected_ssid() {
return web_config::iotWebConf->getWifiSsidParameter()->valueBuffer;
}
char* ampel_password() {
return web_config::iotWebConf->getApPasswordParameter()->valueBuffer;
}
// Sensor
uint16_t &measurement_timestep = web_config::timestepParam.value(); // [s] Value between 2 and 1800 (range for SCD30 sensor).
uint16_t &altitude_above_sea_level = web_config::altitudeParam.value(); // [m]
uint16_t &co2_calibration_level = web_config::atmosphericCO2Param.value(); // [ppm]
bool &auto_calibrate_sensor = web_config::autoCalibrateParam.value(); // [true / false]
float &temperature_offset = web_config::temperatureOffsetParam.value(); // [K] Sign isn't relevant.
bool &is_wifi_on = web_config::ampelWifiParam.value();
uint16_t &wifi_timeout = web_config::wifiTimeoutParam.value();
void save() {
web_config::iotWebConf->saveConfig();
}
// LEDs
uint8_t &max_brightness = web_config::maxBrightnessParam.value();
uint8_t &min_brightness = web_config::minBrightnessParam.value();
uint16_t &led_count = web_config::ledCountParam.value();
// Time server
char *ntp_server = web_config::ntpServerParam.value();
int16_t &time_zone = web_config::timeOffsetParam.value();
bool &daylight_saving_time = web_config::dstParam.value();
// CSV
bool is_csv_active() {
return web_config::csvParams.isActive();
}
uint16_t &csv_interval = web_config::csvIntervalParam.value();
// MQTT
bool is_mqtt_active() {
return web_config::mqttParams.isActive();
}
char *mqtt_server = web_config::mqttServerParam.value();
char *mqtt_user = web_config::mqttUserParam.value();
char *mqtt_password = web_config::mqttPasswordParam.value();
uint16_t &mqtt_port = web_config::mqttPortParam.value();
uint16_t &mqtt_sending_interval = web_config::mqttIntervalParam.value();
bool &mqtt_encryption = web_config::mqttEncryptionParam.value();
bool &allow_mqtt_commands = web_config::mqttCommandsParam.value();
// LORAWAN
#if defined(ESP32)
bool is_lorawan_active() {
return web_config::loraParams.isActive();
}
uint16_t &lorawan_sending_interval = web_config::loraIntervalParam.value();
char *lorawan_device_eui = web_config::deviceEUIParam.value();
char *lorawan_app_key = web_config::appKeyParam.value();
char *lorawan_app_eui = web_config::appEUIParam.value();
#endif
}
#ifndef AMPEL_WEB_CONFIG_H_
#define AMPEL_WEB_CONFIG_H_
#if defined(ESP8266)
# include <ESP8266WebServer.h>
#elif defined(ESP32)
# include <WebServer.h>
#endif
namespace config {
void save(); // Save config to EEPROM
char* ampel_name();
// WiFi
char* selected_ssid();
char* ampel_password(); // For Access Point, and for HTML page
extern bool &is_wifi_on; // [true / false]
extern uint16_t &wifi_timeout; // [s]
// Sensor
extern uint16_t &measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor).
extern uint16_t &altitude_above_sea_level; // [m]
extern uint16_t &co2_calibration_level; // [ppm]
extern bool &auto_calibrate_sensor; // [true / false]
extern float &temperature_offset; // [K] Sign isn't relevant.
// LED
extern uint8_t &max_brightness;
extern uint8_t &min_brightness;
extern uint16_t &led_count;
// Time server
extern char *ntp_server;
extern int16_t &time_zone; // [h]
extern bool &daylight_saving_time; // [true / false]
//CSV
bool is_csv_active(); // [true / false]
extern uint16_t &csv_interval; // [s]
// MQTT
bool is_mqtt_active(); // [true / false]
extern char *mqtt_server;
extern char *mqtt_user;
extern char *mqtt_password;
extern uint16_t &mqtt_port;
extern uint16_t &mqtt_sending_interval; // [s]
extern bool &mqtt_encryption; // [true / false]
extern bool &allow_mqtt_commands; // [true / false]
// HTTP
const char http_user[] = "admin"; // "admin" by default
// LORAWAN
#if defined(ESP32)
bool is_lorawan_active(); // also defined for ESP8266, and set to false
extern uint16_t &lorawan_sending_interval;
extern char *lorawan_device_eui;
extern char *lorawan_app_key;
extern char *lorawan_app_eui;
#endif
// Transmission rate
constexpr uint32_t bauds = 115200;
}
namespace web_config {
void initialize();
void setWifiConnectionCallback(void (*success_function)());
void setWifiFailCallback(void (*fail_function)());
void update();
#if defined(ESP8266)
extern ESP8266WebServer http;
#elif defined(ESP32)
extern WebServer http;
#endif
}
#endif
#include "web_server.h" #include "web_server.h"
#if defined(ESP8266) #include "web_config.h"
# include <ESP8266WebServer.h>
#elif defined(ESP32)
# include <WebServer.h>
#endif
#include "config.h"
#include "util.h" #include "util.h"
#include "ntp.h" #include "ntp.h"
#include "wifi_util.h" #include "wifi_util.h"
#include "co2_sensor.h" #include "co2_sensor.h"
#include "sensor_console.h" #include "sensor_console.h"
#ifdef AMPEL_CSV #include "csv_writer.h"
# include "csv_writer.h" #include "mqtt.h"
#endif #include "lorawan.h"
#ifdef AMPEL_MQTT
# include "mqtt.h"
#endif
#ifdef AMPEL_LORAWAN
# include "lorawan.h"
#endif
namespace config { #if defined(ESP8266)
// Values should be defined in config.h # include <ESP8266WebServer.h>
#ifdef HTTP_USER #elif defined(ESP32)
const char *http_user = HTTP_USER; # include <WebServer.h>
#else
const char *http_user = "";
#endif
#ifdef HTTP_PASSWORD
const char *http_password = HTTP_PASSWORD;
#else
const char *http_password = "";
#endif #endif
}
namespace web_server { namespace web_server {
const char *header_template; const char *header_template;
const char *body_template; const char *body1_template;
const char *body2_template;
const char *script_template; const char *script_template;
void handleWebServerRoot(); void handleWebServerRoot();
void handlePageNotFound(); void handlePageNotFound();
void handleWebServerCommand(); void handleWebServerCommand();
#ifdef AMPEL_CSV
void handleDeleteCSV(); void handleDeleteCSV();
void handleWebServerCSV(); void handleWebServerCSV();
#endif
#if defined(ESP8266) const __FlashStringHelper* showHTMLIf(bool is_active){
ESP8266WebServer http(80); // Create a webserver object that listens for HTTP request on port 80 return is_active ? F("") : F("hidden");
#elif defined(ESP32) }
WebServer http(80);
#endif
void update() { const __FlashStringHelper* yesOrNo(bool is_active){
http.handleClient(); // Listen for HTTP requests from clients return is_active ? F("Yes") : F("No");
} }
void initialize() { void definePages() {
header_template = header_template =
PSTR("<!doctype html><html lang=en>" PSTR("<!doctype html><html lang=en>"
"<head>\n" "<head>"
"<title>%d ppm - CO2 SENSOR - %s - %s</title>\n" "<title>%d ppm - CO2 SENSOR - %s - %s</title>"
"<meta charset='UTF-8'>\n" "<meta charset='UTF-8'>"
// HfT Favicon // HfT Favicon
"<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n" "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>"
// Responsive grid: // Responsive grid:
"<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/pure-min.css'>\n" "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.6/build/pure-min.css'>"
"<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/grids-responsive-min.css'>\n" "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css'>"
// JS Graphs: // JS Graphs:
"<script src='https://cdn.plot.ly/plotly-basic-1.58.2.min.js'></script>\n" "<script src='https://cdn.plot.ly/plotly-basic-2.9.0.min.js'></script>"
// Fullscreen // Fullscreen
"<meta name='viewport' content='width=device-width, initial-scale=1'>\n" "<meta name='viewport' content='width=device-width, initial-scale=1'>"
// Refresh after every measurement. // Refresh after every measurement.
// "<meta http-equiv='refresh' content='%d'>\n" // "<meta http-equiv='refresh' content='%d'>"
"</head>\n" "</head>"
"<body>\n" "<body>"
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><h2 class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> Ampel</h2></div></div>"
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>"
"<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>"
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>"
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>"
"<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>");
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> Ampel</p></div></div>\n" body1_template = PSTR("<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>"
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n" "<li class='pure-menu-item' id='led'>&#11044;</li>" // LED
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n" "</ul></div></div>"
#ifdef AMPEL_CSV "<script>"
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n" // Show a colored dot on the webpage, with a similar color than on LED Ring.
"<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n" "hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;"
"<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>\n" "document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');"
"</script>"
"<div class='pure-g'>"
"<div class='pure-u-1' id='graph'></div>"// Graph placeholder
"</div>"
"<div class='pure-g'>"
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>"
"<tr><th colspan='2'>%s</th></tr>"
"<tr><td>CO<sub>2</sub></td><td>%5d ppm</td></tr>"
"<tr><td>Temperature</td><td>%.1f&#8451;</td></tr>"
"<tr><td>Humidity</td><td>%.1f%%</td></tr>"
"<tr><td>Last measurement</td><td>%s</td></tr>"
"<tr><td>Timestep</td><td>%5d s</td></tr>"
"<tbody %s>"
"<tr><th colspan='2'>CSV</th></tr>"
"<tr><td>Last write</td><td>%s</td></tr>"
"<tr><td>Interval</td><td>%5d s</td></tr>"
"<tr><td>Available space</td><td>%d kB</td></tr>"
"</tbody>"
"<tbody %s>"
"<tr><th colspan='2'>MQTT</th></tr>"
"<tr><td>Connected?</td><td>%s</td></tr>"
"<tr><td>Last publish</td><td>%s</td></tr>"
"<tr><td>Interval</td><td>%5d s</td></tr>"
"</tbody>"
#if defined(ESP32)
"<tbody %s>"
"<tr><th colspan='2'>LoRaWAN</th></tr>"
"<tr><td>Connected?</td><td>%s</td></tr>"
"<tr><td>Frequency</td><td>%s MHz</td></tr>"
"<tr><td>Last transmission</td><td>%s</td></tr>"
"<tr><td>Interval</td><td>%5d s</td></tr>"
"</tbody>"
#endif #endif
"<li class='pure-menu-item' id='led'>&#11044;</li>\n" // LED );
"</ul></div></div>\n"
"<script>\n"
// Show a colored dot on the webpage, with a similar color than on LED Ring.
"hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;\n"
"document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');\n"
"</script>\n"
"<div class='pure-g'>\n"
"<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder
"</div>\n"
"<div class='pure-g'>\n"
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n");
body_template =
PSTR("<tr><th colspan='2'>%s</th></tr>\n"
"<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n"
"<tr><td>Temperature</td><td>%.1f&#8451;</td></tr>\n"
"<tr><td>Humidity</td><td>%.1f%%</td></tr>\n"
"<tr><td>Last measurement</td><td>%s</td></tr>\n"
"<tr><td>Measurement timestep</td><td>%5d s</td></tr>\n"
#ifdef AMPEL_CSV
"<tr><th colspan='2'>CSV</th></tr>\n"
"<tr><td>Last write</td><td>%s</td></tr>\n"
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
"<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
#endif
#ifdef AMPEL_MQTT
"<tr><th colspan='2'>MQTT</th></tr>\n"
"<tr><td>Connected?</td><td>%s</td></tr>\n"
"<tr><td>Last publish</td><td>%s</td></tr>\n"
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
"<tr><th colspan='2'>LoRaWAN</th></tr>\n"
"<tr><td>Connected?</td><td>%s</td></tr>\n"
"<tr><td>Frequency</td><td>%s MHz</td></tr>\n"
"<tr><td>Last transmission</td><td>%s</td></tr>\n"
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
#endif
"<tr><th colspan='2'>Sensor</th></tr>\n"
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
"<tr><td>Auto-calibration?</td><td>%s</td></tr>\n"
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n"
"<tr><td>MAC</td><td>%s</td></tr>\n"
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
"<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n"
"<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
"<tr><td>Board</td><td>%s</td></tr>\n"
"<tr><td>Ampel firmware</td><td>%s</td></tr>\n"
"<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
"</table>\n"
"<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
"<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>\n"
#ifdef AMPEL_CSV
"<form action='/delete_csv' method='POST' onsubmit=\"return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');\">"
"<input type='submit' value='Delete CSV'/>"
"</form>\n"
#endif
"</div>\n");
script_template = body2_template =
PSTR( PSTR(
"<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>\n" "<tr><th colspan='2'>Sensor</th></tr>"
"<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>\n" "<tr><td>Temperature offset</td><td>%.1fK</td></tr>"
#ifdef AMPEL_CSV "<tr><td>Auto-calibration?</td><td>%s</td></tr>"
"<script>\n" "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>"
"document.body.style.cursor = 'default';\n" "<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>"
"fetch('%s',{credentials:'include'})\n" "<tr><td>MAC</td><td>%s</td></tr>"
".then(response=>response.text())\n" "<tr><td>Free heap space</td><td>%6d bytes</td></tr>"
".then(csvText=>csvToTable(csvText))\n" "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>"
".then(htmlTable=>addLogTableToPage(htmlTable))\n" "<tr><td>Frag</td><td>%3d%%</td></tr>"
".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))\n" "<tr><td>Max loop duration</td><td>%5d ms</td></tr>"
".catch(e=>console.error(e));\n" "<tr><td>Board</td><td>%s</td></tr>"
"xs=[];\n" "<tr><td>ID</td><td>%s</td></tr>"
"data=[{x:xs,y:[],type:'scatter',name:'CO<sub>2</sub>',line:{color:'#2ca02c'}},\n" "<tr><td>Ampel firmware</td><td>%s</td></tr>"
"{x:xs,y:[],type:'scatter',name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}},\n" "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>"
"{x:xs,y:[],type:'scatter',name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}];\n" "</table>"
"layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0},\n" "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>"
"xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200},\n" "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>"
"yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3},\n" "<form action='/delete_csv' method='POST' onsubmit=\"return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');\">"
"yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}\n" "<input type='submit' value='Delete CSV'/>"
"};\n" "</form>"
"function csvToTable(csvText) {\n" "<button onclick=\"fetch('/command?send=set_time '+Math.floor(Date.now()/1000))\" %s>Set time!</button>" // Can be useful in AP mode
"csvText=csvText.trim();\n" "</div>"
"lines=csvText.split('\\n');\n" "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>&nbsp;"
"table=document.createElement('table');\n" "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>"
"table.className='pure-table-striped';\n" "<script>"
"n=lines.length;\n" "document.body.style.cursor='default';");
"lines.forEach((line,i)=>{\n"
"fields=line.split(';');\n"
"xs.push(fields[0]);\n"
"data[0]['y'].push(fields[1]);\n"
"data[1]['y'].push(fields[2]);\n"
"data[2]['y'].push(fields[3]);\n"
"if(i>4 && i<n-12){if(i==5){fields=['...','...','...','...']}else{return;}}\n"
"row=document.createElement('tr');\n"
"fields.forEach((field,index)=>{\n"
"cell=document.createElement(i<2?'th':'td');\n"
"cell.appendChild(document.createTextNode(field));\n"
"row.appendChild(cell);});\n"
"table.appendChild(row);});\n"
"return table;}\n"
"function addLogTableToPage(table){document.getElementById('log').appendChild(table);}\n"
"</script>\n"
#endif
"</body>\n"
"</html>");
// Web-server script_template = PSTR("fetch('%s',{credentials:'include'})"
http.on("/", handleWebServerRoot); ".then(r=>r.text())"
http.on("/command", handleWebServerCommand); ".then(c2t)"
#ifdef AMPEL_CSV ".then(t=>document.getElementById('log').appendChild(t))"
http.on(csv_writer::filename, handleWebServerCSV); //NOTE: csv_writer should have been initialized first. ".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))"
http.on("/delete_csv", HTTP_POST, handleDeleteCSV); ".catch(console.error);"
#endif "xs=[];y1=[];y2=[];y3=[];"
http.onNotFound(handlePageNotFound); "d={x:xs,type:'scatter',mode:'lines+markers',marker:{symbol:123}};" // diamond-tall-open from https://plotly.com/python/marker-style/
http.begin(); "data=["
"{...d,...{y:y1,name:'CO<sub>2</sub>',line:{color:'#2ca02c'}}},"
"{...d,...{y:y2,name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}}},"
"{...d,...{y:y3,name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}}];"
"layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0},"
"xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200},"
"yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3},"
"yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}"
"};"
"function c2t(t){"
"t=t.trim();"
"ls=t.split('\\n');"
"tb=document.createElement('table');"
"tb.className='pure-table-striped';"
"n=ls.length;"
"ld=NaN;"
"ls.forEach((l,i)=>{"
"fs=l.split(';');"
//Don't display points without time
"if(fs[0].includes('1970-')){return};"
"d=Date.parse(fs[0]);"
//Split curves when points are more than 1h apart
"if(d-ld>36e5){"
"xs.push(NaN);"
"y1.push(NaN);"
"y2.push(NaN);"
"y3.push(NaN);"
"}"
"ld=d;"
"xs.push(fs[0]);"
"y1.push(fs[1]);"
"y2.push(fs[2]);"
"y3.push(fs[3]);"
"if(i>4&&i<n-12){if(i==5){fs=['...','...','...','...']}else{return;}}"
"r=document.createElement('tr');"
"fs.forEach((f,_)=>{"
"c=document.createElement(i<2?'th':'td');"
"c.appendChild(document.createTextNode(f));"
"r.appendChild(c);});"
"tb.appendChild(r);});"
"return tb;}"
"</script>"
"</body>"
"</html>");
Serial.print(F("You can access this sensor via http://")); // Web-server
Serial.print(ampel.sensorId); web_config::http.on("/", handleWebServerRoot);
Serial.print(F(".local (might be unstable) or http://")); web_config::http.on("/command", handleWebServerCommand);
Serial.println(WiFi.localIP()); web_config::http.on(csv_writer::filename, handleWebServerCSV);
web_config::http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
} }
// Allow access if http_user or http_password are empty, or if provided credentials match /*
* Allow access if Ampel is in access point mode,
* if http_user or http_password are empty,
* or if provided credentials match
*/
bool shouldBeAllowed() { bool shouldBeAllowed() {
return strcmp(config::http_user, "") == 0 || strcmp(config::http_password, "") == 0 return wifi::isAccessPoint() || strcmp(config::http_user, "") == 0 || strcmp(config::ampel_password(), "") == 0
|| http.authenticate(config::http_user, config::http_password); || web_config::http.authenticate(config::http_user, config::ampel_password());
} }
void handleWebServerRoot() { void handleWebServerRoot() {
if (!shouldBeAllowed()) { if (!shouldBeAllowed()) {
return http.requestAuthentication(DIGEST_AUTH); return web_config::http.requestAuthentication(DIGEST_AUTH);
} }
unsigned long ss = seconds(); unsigned long ss = seconds();
...@@ -233,93 +217,89 @@ namespace web_server { ...@@ -233,93 +217,89 @@ namespace web_server {
uint8_t mm = ss / 60; uint8_t mm = ss / 60;
ss -= mm * 60; ss -= mm * 60;
//NOTE: Splitting in multiple parts in order to use less RAM //NOTE: Splitting in multiple parts in order to use less RAM. Higher than 2000 apparently crashes the ESP8266
char content[2000]; // Update if needed char content[1600];
// INFO - Header size : 1767 - Body size : 1991 - Script size : 1909 // Current size (with Lorawan, timesteps and long thing name):
// INFO - Header size : 1347 - Body1 size : 1448 - Body2 size : 1475 - Script size : 1507
snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip snprintf_P(content, sizeof(content), header_template, sensor::co2, config::ampel_name(), wifi::local_ip);
#ifdef AMPEL_CSV
, csv_writer::filename
#endif
);
// Serial.print(F("INFO - Header size : ")); Serial.print(F("INFO - Header size : "));
// Serial.print(strlen(content)); Serial.print(strlen(content));
http.setContentLength(CONTENT_LENGTH_UNKNOWN); web_config::http.setContentLength(CONTENT_LENGTH_UNKNOWN);
http.send_P(200, PSTR("text/html"), content); web_config::http.send_P(200, PSTR("text/html"), content);
// Body // Body
snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature, snprintf_P(content, sizeof(content), body1_template, csv_writer::filename, config::ampel_name(), sensor::co2,
sensor::humidity, sensor::timestamp, config::measurement_timestep, sensor::temperature, sensor::humidity, sensor::timestamp, config::measurement_timestep,
#ifdef AMPEL_CSV showHTMLIf(config::is_csv_active()), csv_writer::last_successful_write, config::csv_interval,
csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024, csv_writer::getAvailableSpace() / 1024, showHTMLIf(config::is_mqtt_active()),
#endif yesOrNo(mqtt::connected), mqtt::last_successful_publish, config::mqtt_sending_interval
#ifdef AMPEL_MQTT #if defined(ESP32)
mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval, , showHTMLIf(config::is_lorawan_active()), yesOrNo(lorawan::connected),
config::lorawan_frequency_plan, lorawan::last_transmission, config::lorawan_sending_interval
#endif #endif
#if defined(AMPEL_LORAWAN) && defined(ESP32) );
lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
config::lorawan_sending_interval,
#endif
config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
wifi::local_ip, wifi::local_ip, ampel.macAddress, ESP.getFreeHeap(), esp_get_max_free_block_size(),
ampel.max_loop_duration, ampel.board, ampel.version, dd, hh, mm, ss);
// Serial.print(F(" - Body size : ")); Serial.print(F(" - Body1 size : "));
// Serial.print(strlen(content)); Serial.print(strlen(content));
http.sendContent(content); web_config::http.sendContent(content);
snprintf_P(content, sizeof(content), body2_template, config::temperature_offset,
yesOrNo(config::auto_calibrate_sensor), config::ampel_name(), config::ampel_name(), wifi::local_ip,
wifi::local_ip, ampel.macAddress, ESP.getFreeHeap(), esp_get_max_free_block_size(),
esp_get_heap_fragmentation(), ampel.max_loop_duration, ampel.board, ampel.sensorId, ampel.version, dd, hh, mm,
ss, showHTMLIf(!ntp::connected_at_least_once));
Serial.print(F(" - Body2 size : "));
Serial.print(strlen(content));
web_config::http.sendContent(content);
// Script // Script
snprintf_P(content, sizeof(content), script_template snprintf_P(content, sizeof(content), script_template, csv_writer::filename, config::ampel_name());
#ifdef AMPEL_CSV
, csv_writer::filename, ampel.sensorId
#endif
);
// Serial.print(F(" - Script size : ")); Serial.print(F(" - Script size : "));
// Serial.println(strlen(content)); Serial.println(strlen(content));
http.sendContent(content); web_config::http.sendContent(content);
} }
#ifdef AMPEL_CSV
void handleWebServerCSV() { void handleWebServerCSV() {
if (!shouldBeAllowed()) { if (!shouldBeAllowed()) {
return http.requestAuthentication(DIGEST_AUTH); return web_config::http.requestAuthentication(DIGEST_AUTH);
} }
if (FS_LIB.exists(csv_writer::filename)) { if (FS_LIB.exists(csv_writer::filename)) {
fs::File csv_file = FS_LIB.open(csv_writer::filename, "r"); fs::File csv_file = FS_LIB.open(csv_writer::filename, "r");
char csv_size[10]; char csv_size[10];
snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size()); snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
http.sendHeader("Content-Length", csv_size); web_config::http.sendHeader("Content-Length", csv_size);
http.streamFile(csv_file, F("text/csv")); web_config::http.streamFile(csv_file, F("text/csv"));
csv_file.close(); csv_file.close();
} else { } else {
http.send(204, F("text/html"), F("No data available.")); web_config::http.send(204, F("text/html"), F("No data available."));
} }
} }
void handleDeleteCSV() { void handleDeleteCSV() {
if (!shouldBeAllowed()) { if (!shouldBeAllowed()) {
return http.requestAuthentication(DIGEST_AUTH); return web_config::http.requestAuthentication(DIGEST_AUTH);
} }
Serial.print(F("Removing CSV file...")); Serial.print(F("Removing CSV file..."));
FS_LIB.remove(csv_writer::filename); FS_LIB.remove(csv_writer::filename);
Serial.println(F(" Done!")); Serial.println(F(" Done!"));
http.sendHeader("Location", "/"); web_config::http.sendHeader("Location", "/");
http.send(303); web_config::http.send(303);
} }
#endif
void handleWebServerCommand() { void handleWebServerCommand() {
if (!shouldBeAllowed()) { if (!shouldBeAllowed()) {
return http.requestAuthentication(DIGEST_AUTH); return web_config::http.requestAuthentication(DIGEST_AUTH);
} }
http.sendHeader("Location", "/"); web_config::http.sendHeader("Location", "/");
http.send(303); web_config::http.send(303);
sensor_console::execute(http.arg("send").c_str()); sensor_console::execute(web_config::http.arg("send").c_str());
} }
void handlePageNotFound() { void handlePageNotFound() {
http.send(404, F("text/plain"), F("404: Not found")); web_config::http.send(404, F("text/plain"), F("404: Not found"));
} }
} }
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
#define WEB_SERVER_H_ #define WEB_SERVER_H_
namespace web_server { namespace web_server {
void initialize(); void definePages();
void update();
} }
#endif #endif
#include "wifi_util.h" #include "wifi_util.h"
#include "config.h" #include "web_config.h"
#include "util.h" #include "util.h"
#include "ntp.h" #include "ntp.h"
#include "led_effects.h" #include "led_effects.h"
...@@ -12,21 +12,42 @@ ...@@ -12,21 +12,42 @@
# include <WiFi.h> # include <WiFi.h>
#endif #endif
namespace config {
// WiFi config. See 'config.h' if you want to modify those values.
const char *wifi_ssid = WIFI_SSID;
const char *wifi_password = WIFI_PASSWORD;
#ifdef WIFI_TIMEOUT
const uint8_t wifi_timeout = WIFI_TIMEOUT; // [s] Will try to connect during wifi_timeout seconds before failing.
#else
const uint8_t wifi_timeout = 60; // [s] Will try to connect during wifi_timeout seconds before failing.
#endif
}
namespace wifi { namespace wifi {
char local_ip[16]; // "255.255.255.255\0" char local_ip[16]; // "255.255.255.255\0"
bool connected() {
return WiFi.status() == WL_CONNECTED;
}
bool isAccessPoint() {
return WiFi.getMode() == WIFI_AP;
}
/*
* Connection attempt, called in blocking mode by setup(). This way, LED effects can be shown
* without needing callbacks, but only during wifi_timeout seconds.
* If connection fails, access point will be started indefinitely, and corresponding
* LED effects will be shown during 5 seconds.
*
* Afterwards, the non-blocking web_config::update() will be called inside loop, and the ampel
* can display CO2 levels.
*/
void tryConnection() {
for (int i = 0; i <= config::wifi_timeout + 5; i++) {
web_config::update();
sensor_console::checkSerialInput(); // To allow reset or ssid ... during startup
if (isAccessPoint()) {
led_effects::alert(0x1cff68);
} else if (connected()) {
break;
} else {
led_effects::showRainbowWheel();
}
Serial.print(".");
}
Serial.println();
}
void scanNetworks() { void scanNetworks() {
Serial.println(); Serial.println();
Serial.println(F("WiFi - Scanning...")); Serial.println(F("WiFi - Scanning..."));
...@@ -49,49 +70,11 @@ namespace wifi { ...@@ -49,49 +70,11 @@ namespace wifi {
Serial.print(F("WiFi - Local IP : ")); Serial.print(F("WiFi - Local IP : "));
Serial.println(wifi::local_ip); Serial.println(wifi::local_ip);
Serial.print(F("WiFi - SSID : ")); Serial.print(F("WiFi - SSID : "));
Serial.println(WIFI_SSID); Serial.println(config::selected_ssid());
} }
bool connected() { void defineCommands() {
return WiFi.status() == WL_CONNECTED;
}
// Initialize Wi-Fi
void connect(const char *hostname) {
sensor_console::defineCommand("wifi_scan", scanNetworks, F("(Scans available WiFi networks)")); sensor_console::defineCommand("wifi_scan", scanNetworks, F("(Scans available WiFi networks)"));
sensor_console::defineCommand("local_ip", showLocalIp, F("(Displays local IP and current SSID)")); sensor_console::defineCommand("local_ip", showLocalIp, F("(Displays local IP and current SSID)"));
//NOTE: WiFi Multi could allow multiple SSID and passwords.
WiFi.persistent(false); // Don't write user & password to Flash.
WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only
#if defined(ESP8266)
WiFi.hostname(hostname);
#elif defined(ESP32)
WiFi.setHostname(hostname);
#endif
Serial.print(F("WiFi - Connecting to "));
Serial.println(config::wifi_ssid);
WiFi.begin(config::wifi_ssid, config::wifi_password);
// Wait for connection, at most wifi_timeout seconds
for (int i = 0; i <= config::wifi_timeout && (WiFi.status() != WL_CONNECTED); i++) {
led_effects::showRainbowWheel();
Serial.print(".");
}
if (connected()) {
led_effects::showKITTWheel(color::green);
Serial.println();
Serial.print(F("WiFi - Connected! IP address: "));
IPAddress address = WiFi.localIP();
snprintf(local_ip, sizeof(local_ip), "%d.%d.%d.%d", address[0], address[1], address[2], address[3]);
Serial.println(local_ip);
} else {
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
led_effects::showKITTWheel(color::red);
Serial.print(F("Connection to WiFi failed! Status : "));
Serial.println(WiFi.status());
}
} }
} }
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
#define WIFI_UTIL_H_INCLUDED #define WIFI_UTIL_H_INCLUDED
namespace wifi { namespace wifi {
extern char local_ip[]; extern char local_ip[16];
void connect(const char *hostname); void defineCommands();
bool connected(); bool connected();
bool isAccessPoint();
void tryConnection();
} }
#endif #endif
; PlatformIO Project Configuration File ; PlatformIO Project Configuration File
; ;
; Build options: build flags, source filter, extra scripting ; Build options: build flags, source filter
; Upload options: custom port, speed and extra flags ; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages ; Library options: dependencies, extra library storages
; Advanced options: extra scripting
; ;
; Please visit documentation for the other options and examples ; Please visit documentation for the other options and examples
; http://docs.platformio.org/page/projectconf.html ; https://docs.platformio.org/page/projectconf.html
[platformio] [platformio]
src_dir = ampel-firmware src_dir = ampel-firmware
...@@ -21,6 +22,9 @@ platform = espressif32 ...@@ -21,6 +22,9 @@ platform = espressif32
board = ttgo-lora32-v1 board = ttgo-lora32-v1
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
lib_deps = lib_deps =
MCCI LoRaWAN LMIC library MCCI LoRaWAN LMIC library
build_flags =
-D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
-D CFG_eu868=1
-D CFG_sx1276_radio=1
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment