Browse Source

light: rework hsv input in webui

* make sure we don't apply hsv and brightnesss, only take hue and saturation
  when calculating inputs. use value as brightness percentage, wrapping
  the original function (which also makes other modules use this codepath)
* deduce interface elements based on both setting and incoming
  properties. wrap everything in {light: ...} in both inputs and
  outputs, alse allow to update multiple properties at the same time
* add light state toggle when special lights relay is not set up
* remove channel, state and brightness elements through css styling
  instead of going to the elements directly. no need to chech whether
  certain elements exist
pull/2552/head
Maxim Prokhorov 1 year ago
parent
commit
2a7cabb4d3
27 changed files with 16102 additions and 15883 deletions
  1. BIN
      code/espurna/data/index.all.html.gz
  2. BIN
      code/espurna/data/index.curtain.html.gz
  3. BIN
      code/espurna/data/index.garland.html.gz
  4. BIN
      code/espurna/data/index.light.html.gz
  5. BIN
      code/espurna/data/index.lightfox.html.gz
  6. BIN
      code/espurna/data/index.rfbridge.html.gz
  7. BIN
      code/espurna/data/index.rfm69.html.gz
  8. BIN
      code/espurna/data/index.sensor.html.gz
  9. BIN
      code/espurna/data/index.small.html.gz
  10. BIN
      code/espurna/data/index.thermostat.html.gz
  11. +211
    -153
      code/espurna/light.cpp
  12. +22
    -33
      code/espurna/light.h
  13. +2284
    -2269
      code/espurna/static/index.all.html.gz.h
  14. +1431
    -1427
      code/espurna/static/index.curtain.html.gz.h
  15. +1374
    -1369
      code/espurna/static/index.garland.html.gz.h
  16. +1985
    -1969
      code/espurna/static/index.light.html.gz.h
  17. +1396
    -1390
      code/espurna/static/index.lightfox.html.gz.h
  18. +1425
    -1421
      code/espurna/static/index.rfbridge.html.gz.h
  19. +1428
    -1423
      code/espurna/static/index.rfm69.html.gz.h
  20. +1528
    -1524
      code/espurna/static/index.sensor.html.gz.h
  21. +1366
    -1361
      code/espurna/static/index.small.html.gz.h
  22. +1406
    -1401
      code/espurna/static/index.thermostat.html.gz.h
  23. +3
    -2
      code/espurna/system.cpp
  24. +2
    -1
      code/espurna/web_utils.h
  25. +14
    -9
      code/html/custom.css
  26. +219
    -123
      code/html/custom.js
  27. +8
    -8
      code/html/index.html

BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.curtain.html.gz View File


BIN
code/espurna/data/index.garland.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.lightfox.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


BIN
code/espurna/data/index.thermostat.html.gz View File


+ 211
- 153
code/espurna/light.cpp View File

@ -19,6 +19,7 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#include "ws.h"
#include <ArduinoJson.h>
#include "web_utils.h"
#include <array>
#include <cstring>
@ -735,6 +736,16 @@ std::unique_ptr<LightProvider> _light_provider;
namespace {
void _lightBrightnessPercent(long percent) {
const auto fixed = std::clamp(percent, 0l, 100l);
const auto ratio = espurna::light::BrightnessMax * fixed;
lightBrightness(ratio / 100l);
}
long _lightBrightnessPercent() {
return (_light_brightness * 100l) / espurna::light::BrightnessMax;
}
// After the channel value was updated through the API (i.e. through changing the `inputValue`),
// these functions are expected to be called. Which one is chosen is based on the current settings values.
// TODO: existing mapping class handles setting `inputValue` & getting `target` value applied by the transition handler
@ -1095,29 +1106,35 @@ void _lightFromRgbPayload(espurna::StringView payload) {
_lightFromCommaSeparatedPayload(payload);
}
void _lightFromHsvPayload(espurna::StringView payload) {
if (!_light_has_color || !payload.length()) {
return;
}
long hsv[3] {0, 0, 0};
auto it = std::begin(hsv);
espurna::light::Hsv _lightHsvFromPayload(espurna::StringView payload) {
espurna::light::Hsv::Array values;
auto it = std::begin(values);
// HSV string is expected to be "H,S,V", where:
// - H [0...360]
// - S [0...100]
// - V [0...100]
const auto parsed = _lightApplyForEachToken(
payload, ',', it, std::end(hsv));
payload, ',', it, std::end(values));
// discard partial or uneven payloads
if ((parsed != payload.end()) || (it != std::end(hsv))) {
espurna::light::Hsv out;
if ((parsed != payload.end()) || (it != std::end(values))) {
return out;
}
// values are expected to be 'clamped' either in the
// following call or in ctor of the helper object
out = espurna::light::Hsv(values);
return out;
}
void _lightFromHsvPayload(espurna::StringView payload) {
if (!_light_has_color || !payload.length()) {
return;
}
// values are expected to be 'clamped' either
// in the call or in ctor of the helper object
lightHsv({hsv[0], hsv[1], hsv[2]});
lightHsv(_lightHsvFromPayload(payload));
}
// Thanks to Sacha Telgenhof for sharing this code in his AiLight library
@ -1296,25 +1313,91 @@ void _lightFromGroupPayload(espurna::StringView payload) {
}
}
// HSV to RGB transformation
//
// INPUT: [0,100,57]
// IS: [145,0,0]
// SHOULD: [255,0,0]
espurna::light::Rgb _lightRgb(espurna::light::Hsv hsv) {
constexpr auto ValueMin = static_cast<double>(espurna::light::ValueMin);
double r { ValueMin };
double g { ValueMin };
double b { ValueMin };
constexpr auto Scale = 100.0;
auto v = static_cast<double>(hsv.value()) / Scale;
if (hsv.saturation()) {
auto h = hsv.hue();
if (h < 0) {
h = 0;
} else if (h >= 360) {
h = 359;
}
auto s = static_cast<double>(hsv.saturation()) / Scale;
auto c = v * s;
auto hmod2 = fs_fmod(static_cast<double>(h) / 60.0, 2.0);
auto x = c * (1.0 - std::abs(hmod2 - 1.0));
auto m = v - c;
if ((0 <= h) && (h < 60)) {
r = c;
g = x;
} else if ((60 <= h) && (h < 120)) {
r = x;
g = c;
} else if ((120 <= h) && (h < 180)) {
g = c;
b = x;
} else if ((180 <= h) && (h < 240)) {
g = x;
b = c;
} else if ((240 <= h) && (h < 300)) {
r = x;
b = c;
} else if ((300 <= h) && (h < 360)) {
r = c;
b = x;
}
constexpr auto ValueMax = static_cast<double>(espurna::light::ValueMax);
r = (r + m) * ValueMax;
g = (g + m) * ValueMax;
b = (b + m) * ValueMax;
}
return espurna::light::Rgb(
static_cast<long>(std::nearbyint(r)),
static_cast<long>(std::nearbyint(g)),
static_cast<long>(std::nearbyint(b)));
}
espurna::light::Hsv _lightHsv(espurna::light::Rgb rgb) {
auto r = static_cast<double>(rgb.red()) / espurna::light::ValueMax;
auto g = static_cast<double>(rgb.green()) / espurna::light::ValueMax;
auto b = static_cast<double>(rgb.blue()) / espurna::light::ValueMax;
using namespace espurna::light;
auto max = std::max({r, g, b});
auto min = std::min({r, g, b});
const auto r = static_cast<double>(rgb.red()) / ValueMax;
const auto g = static_cast<double>(rgb.green()) / ValueMax;
const auto b = static_cast<double>(rgb.blue()) / ValueMax;
const auto max = std::max({r, g, b});
const auto min = std::min({r, g, b});
auto v = max;
if (min != max) {
auto s = (max - min) / max;
auto delta = max - min;
auto s = delta / max;
auto rc = (max - r) / delta;
auto gc = (max - g) / delta;
auto bc = (max - b) / delta;
double h { 0.0 };
double h;
if (r == max) {
h = bc - gc;
} else if (g == max) {
@ -1328,23 +1411,19 @@ espurna::light::Hsv _lightHsv(espurna::light::Rgb rgb) {
h = 1.0 + h;
}
return espurna::light::Hsv(
return Hsv(
std::lround(h * 360.0),
std::lround(s * 100.0),
std::lround(v * 100.0));
}
return espurna::light::Hsv(espurna::light::Hsv::HueMin, espurna::light::Hsv::SaturationMin, v);
return Hsv(Hsv::HueMin, Hsv::SaturationMin, v);
}
String _lightHsvPayload(espurna::light::Rgb rgb) {
String _lightHsvPayload(espurna::light::Hsv hsv) {
String out;
out.reserve(12);
auto hsv = _lightHsv(rgb);
long values[3] {hsv.hue(), hsv.saturation(), hsv.value()};
auto values = hsv.asArray();
for (const auto& value : values) {
if (out.length()) {
out += ',';
@ -1355,6 +1434,10 @@ String _lightHsvPayload(espurna::light::Rgb rgb) {
return out;
}
String _lightHsvPayload(espurna::light::Rgb rgb) {
return _lightHsvPayload(_lightHsv(rgb));
}
String _lightHsvPayload() {
return _lightHsvPayload(_lightToTargetRgb());
}
@ -2067,11 +2150,11 @@ bool _lightApiTransition(espurna::StringView payload) {
}
int _lightMqttReportMask() {
return espurna::light::DefaultReport & ~(static_cast<int>(mqttForward() ? espurna::light::Report::None : espurna::light::Report::Mqtt));
return espurna::light::Report::Default & ~(mqttForward() ? espurna::light::Report::None : espurna::light::Report::Mqtt);
}
int _lightMqttReportGroupMask() {
return _lightMqttReportMask() & ~static_cast<int>(espurna::light::Report::MqttGroup);
return _lightMqttReportMask() & ~espurna::light::Report::MqttGroup;
}
void _lightUpdateFromMqtt(LightTransition transition) {
@ -2389,42 +2472,54 @@ bool _lightWebSocketOnKeyCheck(espurna::StringView key, const JsonVariant&) {
}
void _lightWebSocketStatus(JsonObject& root) {
JsonObject& light = root.createNestedObject("light");
if (_light_use_color) {
const auto rgb = _lightToInputRgb();
if (_light_use_rgb) {
root["rgb"] = _lightRgbHexPayload(_lightToInputRgb());
light["rgb"] = _lightRgbHexPayload(rgb);
} else {
root["hsv"] = _lightHsvPayload(_lightToTargetRgb());
const auto hsv = _lightHsv(rgb);
light["hsv"] = _lightHsvPayload(espurna::light::Hsv(
hsv.hue(), hsv.saturation(), _lightBrightnessPercent()));
}
}
if (_light_use_cct) {
JsonObject& mireds = root.createNestedObject("mireds");
mireds["value"] = _light_mireds;
mireds["cold"] = _light_cold_mireds;
mireds["warm"] = _light_warm_mireds;
root["useCCT"] = _light_use_cct;
light["mireds"] = _light_mireds;
}
JsonArray& channels = root.createNestedArray("channels");
JsonArray& values = light.createNestedArray("values");
for (auto& channel : _light_channels) {
channels.add(channel.inputValue);
values.add(channel.inputValue);
}
root["brightness"] = _light_brightness;
root["lightstate"] = _light_state;
light["brightness"] = _light_brightness;
light["state"] = _light_state;
}
void _lightWebSocketOnVisible(JsonObject& root) {
wsPayloadModule(root, PSTR("light"));
JsonObject& light = root.createNestedObject("light");
light["channels"] = _light_channels.size();
light["mode"] = _light_use_rgb ? "rgb" : "hsv";
if (_light_use_cct) {
JsonObject& cct = light.createNestedObject("cct");
cct["cold"] = _light_cold_mireds;
cct["warm"] = _light_warm_mireds;
}
}
void _lightWebSocketOnConnected(JsonObject& root) {
root["mqttGroupColor"] = espurna::light::settings::mqttGroup();
root["useCCT"] = _light_use_cct;
root["useColor"] = _light_use_color;
root["useWhite"] = _light_use_white;
root["useGamma"] = _light_use_gamma;
root["useTransitions"] = _light_use_transitions;
root["useRGB"] = _light_use_rgb;
root["useTransitions"] = _light_use_transitions;
root["useWhite"] = _light_use_white;
root["ltSave"] = _light_save;
root["ltSaveDelay"] = _light_save_delay.count();
root["ltTime"] = _light_transition_time.count();
@ -2435,45 +2530,61 @@ void _lightWebSocketOnConnected(JsonObject& root) {
}
void _lightWebSocketOnAction(uint32_t client_id, const char* action, JsonObject& data) {
STRING_VIEW_INLINE(Color, "color");
STRING_VIEW_INLINE(Light, "light");
if (Light != action) {
return;
}
if (_light_has_color) {
if (Color == action) {
STRING_VIEW_INLINE(Rgb, "rgb");
STRING_VIEW_INLINE(Hsv, "hsv");
bool update { false };
if (data.containsKey(Rgb)) {
_lightFromRgbPayload(data[Rgb].as<String>());
lightUpdate();
} else if (data.containsKey(Hsv)) {
_lightFromHsvPayload(data[Hsv].as<String>());
lightUpdate();
}
}
STRING_VIEW_INLINE(State, "state");
if (data.containsKey("state")) {
lightState(data[State].as<bool>());
update = true;
}
STRING_VIEW_INLINE(Mireds, "mireds");
STRING_VIEW_INLINE(Brightness, "brightness");
STRING_VIEW_INLINE(Id, "id");
if (data.containsKey(Brightness)) {
lightBrightness(data[Brightness].as<long>());
update = true;
}
STRING_VIEW_INLINE(Rgb, "rgb");
if (data.containsKey(Rgb)) {
_lightFromRgbPayload(data[Rgb].as<String>());
update = true;
}
STRING_VIEW_INLINE(Hsv, "hsv");
if (data.containsKey(Hsv)) {
lightHsv(_lightHsvFromPayload(data[Hsv].as<String>()));
update = true;
}
STRING_VIEW_INLINE(Mireds, "mireds");
if (data.containsKey(Mireds)) {
_fromMireds(data[Mireds].as<long>());
update = true;
}
STRING_VIEW_INLINE(Channel, "channel");
STRING_VIEW_INLINE(Value, "value");
JsonObject& channel = data[Channel];
if (Mireds == action) {
if (data.containsKey(Mireds)) {
_fromMireds(data[Mireds].as<long>());
lightUpdate();
}
} else if (Channel == action) {
if (data.containsKey(Id) && data.containsKey(Value)) {
lightChannel(data[Id].as<size_t>(), data[Value].as<long>());
lightUpdate();
}
} else if (Brightness == action) {
if (data.containsKey(Value)) {
lightBrightness(data[Value].as<long>());
lightUpdate();
if (channel.success()) {
for (auto& kv : channel) {
const auto id = parseUnsigned(kv.key, 10);
if (!id.ok) {
break;
}
lightChannel(id.value, kv.value.as<long>());
update = true;
}
}
if (update) {
lightUpdate();
}
}
} // namespace
@ -2611,7 +2722,8 @@ static void _lightCommand(::terminal::CommandContext&& ctx) {
lightUpdate();
}
ctx.output.printf("%s\n", _light_state ? "ON" : "OFF");
ctx.output.printf_P(PSTR("%s\n"),
_light_state ? PSTR("ON") : PSTR("OFF"));
terminalOK(ctx);
}
@ -2622,7 +2734,7 @@ static void _lightCommandBrightness(::terminal::CommandContext&& ctx) {
_lightAdjustBrightness(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf("%ld\n", _light_brightness);
ctx.output.printf_P(PSTR("%ld\n"), _light_brightness);
terminalOK(ctx);
}
@ -2666,15 +2778,23 @@ static void _lightCommandChannel(::terminal::CommandContext&& ctx) {
alignas(4) static constexpr char LightCommandRgb[] PROGMEM = "RGB";
static void _lightCommandColors(const ::terminal::CommandContext& ctx) {
const auto rgb = _lightToTargetRgb();
ctx.output.printf_P(PSTR("hsv %s\n"),
_lightHsvPayload(rgb).c_str());
ctx.output.printf_P(PSTR("rgb %s\n"),
_lightRgbPayload(rgb).c_str());
terminalOK(ctx);
}
static void _lightCommandRgb(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightFromRgbPayload(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf_P(PSTR("rgb %s\n"),
_lightRgbPayload(_lightToTargetRgb()).c_str());
terminalOK(ctx);
_lightCommandColors(ctx);
}
alignas(4) static constexpr char LightCommandHsv[] PROGMEM = "HSV";
@ -2685,9 +2805,7 @@ static void _lightCommandHsv(::terminal::CommandContext&& ctx) {
lightUpdate();
}
ctx.output.printf_P(PSTR("hsv %s\n"),
_lightHsvPayload().c_str());
terminalOK(ctx);
_lightCommandColors(ctx);
}
alignas(4) static constexpr char LightCommandKelvin[] PROGMEM = "KELVIN";
@ -2773,68 +2891,16 @@ void lightRgb(espurna::light::Rgb rgb) {
_light_mapping.blue(rgb.blue());
}
// HSV to RGB transformation -----------------------------------------------
//
// INPUT: [0,100,57]
// IS: [145,0,0]
// SHOULD: [255,0,0]
void lightHsv(espurna::light::Hsv hsv) {
double r { 0.0 };
double g { 0.0 };
double b { 0.0 };
auto v = static_cast<double>(hsv.value()) / 100.0;
if (hsv.saturation()) {
auto h = hsv.hue();
if (h < 0) {
h = 0;
} else if (h >= 360) {
h = 359;
}
auto s = static_cast<double>(hsv.saturation()) / 100.0;
auto c = v * s;
auto hmod2 = fs_fmod(static_cast<double>(h) / 60.0, 2.0);
auto x = c * (1.0 - std::abs(hmod2 - 1.0));
auto m = v - c;
if ((0 <= h) && (h < 60)) {
r = c;
g = x;
} else if ((60 <= h) && (h < 120)) {
r = x;
g = c;
} else if ((120 <= h) && (h < 180)) {
g = c;
b = x;
} else if ((180 <= h) && (h < 240)) {
g = x;
b = c;
} else if ((240 <= h) && (h < 300)) {
r = x;
b = c;
} else if ((300 <= h) && (h < 360)) {
r = c;
b = x;
}
r = (r + m) * 255.0;
g = (g + m) * 255.0;
b = (b + m) * 255.0;
}
_light_mapping.red(std::lround(r));
_light_mapping.green(std::lround(g));
_light_mapping.blue(std::lround(b));
void lightHs(long hue, long saturation) {
lightRgb(_lightRgb(
espurna::light::Hsv(
hue, saturation,
espurna::light::Hsv::ValueMax)));
}
void lightHs(long hue, long saturation) {
lightHsv({hue, saturation, espurna::light::Hsv::ValueMax});
void lightHsv(espurna::light::Hsv hsv) {
lightHs(hsv.hue(), hsv.saturation());
lightBrightnessPercent(hsv.value());
}
espurna::light::Hsv lightHsv() {
@ -2993,16 +3059,12 @@ void _lightUpdate(LightTransition transition, int report, bool save) {
_lightUpdate(transition, report, save, false);
}
void _lightUpdate(LightTransition transition, espurna::light::Report report, bool save) {
_lightUpdate(transition, static_cast<int>(report), save, false);
}
void _lightUpdate(LightTransition transition) {
_lightUpdate(transition, espurna::light::DefaultReport, _light_save, false);
_lightUpdate(transition, espurna::light::Report::Default, _light_save, false);
}
void _lightUpdate(bool save) {
_lightUpdate(lightTransition(), espurna::light::DefaultReport, save, false);
_lightUpdate(lightTransition(), espurna::light::Report::Default, save, false);
}
} // namespace
@ -3012,17 +3074,13 @@ void lightSequence(LightSequenceCallbacks callbacks) {
}
void lightUpdateSequence(LightTransition transition) {
_lightUpdate(transition, 0, false, true);
_lightUpdate(transition, espurna::light::Report::None, false, true);
}
void lightUpdate(LightTransition transition, int report, bool save) {
_lightUpdate(transition, report, save);
}
void lightUpdate(LightTransition transition, espurna::light::Report report, bool save) {
_lightUpdate(transition, report, save);
}
void lightUpdate(LightTransition transition) {
_lightUpdate(transition);
}
@ -3159,7 +3217,7 @@ long lightBrightness() {
}
void lightBrightnessPercent(long percent) {
lightBrightness((percent / 100l) * espurna::light::BrightnessMax);
_lightBrightnessPercent(percent);
}
void lightBrightness(long brightness) {


+ 22
- 33
code/espurna/light.h View File

@ -38,35 +38,13 @@ constexpr long BrightnessMax { LIGHT_MAX_BRIGHTNESS };
constexpr long MiredsCold { LIGHT_COLDWHITE_MIRED };
constexpr long MiredsWarm { LIGHT_WARMWHITE_MIRED };
enum class Report {
None = 0,
Web = 1 << 0,
Mqtt = 1 << 1,
MqttGroup = 1 << 2
};
constexpr int operator|(Report lhs, int rhs) {
return static_cast<int>(lhs) | rhs;
}
constexpr int operator|(int lhs, Report rhs) {
return lhs | static_cast<int>(rhs);
}
constexpr int operator|(Report lhs, Report rhs) {
return static_cast<int>(lhs) | static_cast<int>(rhs);
}
constexpr int operator&(int lhs, Report rhs) {
return lhs & static_cast<int>(rhs);
}
constexpr int operator&(Report lhs, int rhs) {
return static_cast<int>(lhs) & rhs;
}
struct Report {
static constexpr int None = 0;
static constexpr int Web = 1 << 0;
static constexpr int Mqtt = 1 << 1;
static constexpr int MqttGroup = 1 << 2;
constexpr int DefaultReport {
Report::Web | Report::Mqtt | Report::MqttGroup
static constexpr int Default { Web | Mqtt | MqttGroup };
};
struct Hsv {
@ -79,6 +57,8 @@ struct Hsv {
static constexpr long ValueMin { 0 };
static constexpr long ValueMax { 100 };
using Array = std::array<long, 3>;
Hsv() = default;
Hsv(const Hsv&) = default;
Hsv(Hsv&&) = default;
@ -86,24 +66,34 @@ struct Hsv {
Hsv& operator=(const Hsv&) = default;
Hsv& operator=(Hsv&&) = default;
Hsv(long hue, long saturation, long value) :
constexpr explicit Hsv(Array array) noexcept :
_hue(array[0]),
_saturation(array[1]),
_value(array[2])
{}
constexpr Hsv(long hue, long saturation, long value) noexcept :
_hue(std::clamp(hue, HueMin, HueMax)),
_saturation(std::clamp(saturation, SaturationMin, SaturationMax)),
_value(std::clamp(value, ValueMin, ValueMax))
{}
long hue() const {
constexpr long hue() const {
return _hue;
}
long saturation() const {
constexpr long saturation() const {
return _saturation;
}
long value() const {
constexpr long value() const {
return _value;
}
constexpr Array asArray() const {
return Array{{_hue, _saturation, _value}};
}
private:
long _hue { HueMin };
long _saturation { SaturationMin };
@ -257,7 +247,6 @@ void lightBrightnessStep(long steps, long multiplier);
void lightChannelStep(size_t id, long steps);
void lightChannelStep(size_t id, long steps, long multiplier);
void lightUpdate(LightTransition transition, espurna::light::Report report, bool save);
void lightUpdate(LightTransition transition, int report, bool save);
void lightUpdate(LightTransition transition);
void lightUpdate(bool save);


+ 2284
- 2269
code/espurna/static/index.all.html.gz.h
File diff suppressed because it is too large
View File


+ 1431
- 1427
code/espurna/static/index.curtain.html.gz.h
File diff suppressed because it is too large
View File


+ 1374
- 1369
code/espurna/static/index.garland.html.gz.h
File diff suppressed because it is too large
View File


+ 1985
- 1969
code/espurna/static/index.light.html.gz.h
File diff suppressed because it is too large
View File


+ 1396
- 1390
code/espurna/static/index.lightfox.html.gz.h
File diff suppressed because it is too large
View File


+ 1425
- 1421
code/espurna/static/index.rfbridge.html.gz.h
File diff suppressed because it is too large
View File


+ 1428
- 1423
code/espurna/static/index.rfm69.html.gz.h
File diff suppressed because it is too large
View File


+ 1528
- 1524
code/espurna/static/index.sensor.html.gz.h
File diff suppressed because it is too large
View File


+ 1366
- 1361
code/espurna/static/index.small.html.gz.h
File diff suppressed because it is too large
View File


+ 1406
- 1401
code/espurna/static/index.thermostat.html.gz.h
File diff suppressed because it is too large
View File


+ 3
- 2
code/espurna/system.cpp View File

@ -403,7 +403,7 @@ void SystemTimer::callback() {
}
void SystemTimer::schedule_once(Duration duration, Callback callback) {
once(duration, [callback = std::move(callback)]() {
once(duration, [callback]() {
espurnaRegisterOnce(callback);
});
}
@ -980,7 +980,8 @@ void run() {
void stop(Callback callback) {
auto found = std::remove_if(
internal::runners.begin(), internal::runners.end(),
internal::runners.begin(),
internal::runners.end(),
[&](const CallbackRunner& runner) {
return callback == runner.callback;
});


+ 2
- 1
code/espurna/web_utils.h View File

@ -23,7 +23,8 @@ namespace Internals {
template <>
struct StringTraits<::espurna::StringView, void> {
static bool equals(::espurna::StringView lhs, const char* rhs) {
template <typename T>
static bool equals(::espurna::StringView lhs, T&& rhs) {
return lhs == rhs;
}


+ 14
- 9
code/html/custom.css View File

@ -460,15 +460,6 @@ input[disabled] + .toggle .toggle__handler {
text-decoration: none;
}
/* -----------------------------------------------------------------------------
Home panel
-------------------------------------------------------------------------- */
#color {
padding-bottom: 1em;
padding-top: 1em;
}
/* -----------------------------------------------------------------------------
Admin panel
-------------------------------------------------------------------------- */
@ -600,3 +591,17 @@ input::placeholder {
width: 100%;
box-shadow: 0 1px 2px 1px rgb(192 0 0 / 25%)
}
/* -----------------------------------------------------------------------------
Lights
-------------------------------------------------------------------------- */
.IroColorPicker {
padding-bottom: 1em;
padding-top: 1em;
transition: visibility 0.2s;
}
.IroColorPicker, .light-control {
content-visibility: hidden;
}

+ 219
- 123
code/html/custom.js View File

@ -403,21 +403,25 @@ const groupSettingsHandler = {
// elements must be re-enumerated and assigned new id's to remain unique
// (and avoid having one line's checkbox affecting some other one)
function createCheckboxes(node) {
const checkboxes = node.querySelectorAll("input[type='checkbox']");
for (const checkbox of checkboxes) {
checkbox.id = checkbox.name;
checkbox.parentElement.classList.add("toggleWrapper");
function createCheckbox(checkbox) {
checkbox.id = checkbox.name;
checkbox.parentElement.classList.add("toggleWrapper");
let label = document.createElement("label");
label.classList.add("toggle");
label.htmlFor = checkbox.id;
const label = document.createElement("label");
label.classList.add("toggle");
label.htmlFor = checkbox.id;
let span = document.createElement("span");
span.classList.add("toggle__handler");
label.appendChild(span);
const span = document.createElement("span");
span.classList.add("toggle__handler");
label.appendChild(span);
checkbox.parentElement.appendChild(label);
}
checkbox.parentElement.appendChild(label);
function createCheckboxes(node) {
const checkboxes = node.querySelectorAll("input[type='checkbox']");
for (const checkbox of checkboxes) {
createCheckbox(checkbox);
}
}
@ -894,17 +898,29 @@ function initSetupPassword(form) {
});
}
function moduleVisible(module) {
function styleInject(rules) {
const style = document.createElement("style");
style.setAttribute("type", "text/css");
document.head.appendChild(style);
let pos = style.sheet.rules.length;
for (let rule of rules) {
style.sheet.insertRule(rule, pos++);
}
}
function styleVisible(selector, value) {
return `${selector} { content-visibility: ${value ? "visible": "hidden"}; }`
}
function moduleVisible(module) {
if (module === "sch") {
style.sheet.insertRule(`li.module-${module} { display: inherit; }`, pos++);
style.sheet.insertRule(`div.module-${module} { display: flex; }`, pos++);
styleInject([
`li.module-${module} { display: inherit; }`,
`div.module-${module} { display: flex; }`
]);
} else {
style.sheet.insertRule(`.module-${module} { display: inherit; }`, pos++);
styleInject([`.module-${module} { display: inherit; }`]);
}
}
@ -2149,138 +2165,224 @@ function colorBox() {
return {component: iro.ui.Box, options: {}};
}
function updateColor(mode, value) {
if (ColorPicker) {
if (mode === "rgb") {
ColorPicker.color.hexString = value;
} else if (mode === "hsv") {
ColorPicker.color.hsv = hsvStringToColor(value);
}
return;
function colorUpdate(mode, value) {
if ("rgb" === mode) {
ColorPicker.color.hexString = value;
} else if ("hsv" === mode) {
ColorPicker.color.hsv = hsvStringToColor(value);
}
}
function showLightState(value) {
styleInject([
styleVisible(".light-control", !value)
]);
}
// TODO: useRGB -> ltWheel?
// TODO: always show wheel + sliders like before?
var layout = []
if (mode === "rgb") {
function initLightState() {
const toggle = document.getElementById("light-state");
toggle.addEventListener("change", (event) => {
event.preventDefault();
sendAction("light", {state: event.target.checked});
});
}
function updateLightState(value) {
const state = document.getElementById("light-state");
state.checked = value;
const picker = document.querySelector(".IroColorPicker");
picker.style["content-visibility"] = value ? "visible" : "hidden";
}
function colorEnabled(value) {
styleInject([
styleVisible(".IroColorPicker", value),
styleVisible(".light-channel", !value)
]);
}
function colorInit(mode) {
// TODO: ref. #2451, input:change causes pretty fast updates.
// either make sure we don't cause any issue on the esp, or switch to
// color:change instead (which applies after input ends)
let change = () => {
};
const layout = []
switch (mode) {
case "rgb":
layout.push(colorWheel());
layout.push(colorSlider("value"));
} else if (mode === "hsv") {
change = (color) => {
sendAction("light", {
rgb: color.hexString
});
};
break;
case "hsv":
layout.push(colorBox());
layout.push(colorSlider("hue"));
layout.push(colorSlider("saturation"));
layout.push(colorSlider("value"));
change = (color) => {
sendAction("light", {
hsv: colorToHsvString(color)
});
};
break;
default:
return;
}
var options = {
color: (mode === "rgb") ? value : hsvStringToColor(value),
layout: layout
};
const picker = document.createElement("div");
picker.classList.add("pure-control-group");
ColorPicker = new iro.ColorPicker(picker, {layout});
ColorPicker.on("input:change", change);
// TODO: ref. #2451, this causes pretty fast updates.
// since we immediatly start the transition, debug print's yield() may interrupt us mid initialization
// api could also wait and hold the value for a bit, applying only some of the values between start and end, and then apply the last one
ColorPicker = new iro.ColorPicker("#color", options);
ColorPicker.on("input:change", (color) => {
if (mode === "rgb") {
sendAction("color", {rgb: color.hexString});
} else if (mode === "hsv") {
sendAction("color", {hsv: colorToHsvString(color)});
}
});
const container = document.getElementById("light");
container.appendChild(picker);
}
function onChannelSliderChange(event) {
event.target.nextElementSibling.textContent = event.target.value;
sendAction("channel", {id: event.target.dataset["id"], value: event.target.value});
}
function initMireds(value) {
if (!value) {
return;
}
function onBrightnessSliderChange(event) {
event.target.nextElementSibling.textContent = event.target.value;
sendAction("brightness", {value: event.target.value});
const control = loadTemplate("mireds-control");
const slider = control.getElementById("mireds");
slider.addEventListener("change", (event) => {
event.target.nextElementSibling.textContent = event.target.value;
sendAction("light", {mireds: event.target.value});
});
mergeTemplate(document.getElementById("cct"), control);
// When there are CCT controls, no need for raw white channel sliders
let rules = [];
for (let channel = 3; channel < 5; ++channel) {
rules.push(
styleVisible(`.light-channel input.slider[data-id='${channel}']`, false));
}
styleInject(rules);
}
function updateMireds(value) {
let mireds = document.getElementById("mireds");
const mireds = document.getElementById("mireds");
if (mireds !== null) {
mireds.setAttribute("min", value.cold);
mireds.setAttribute("max", value.warm);
mireds.value = value.value;
mireds.nextElementSibling.textContent = value.value;
return;
}
}
let control = loadTemplate("mireds-control");
control.getElementById("mireds").addEventListener("change", (event) => {
event.target.nextElementSibling.textContent = event.target.value;
sendAction("mireds", {mireds: event.target.value});
});
mergeTemplate(document.getElementById("cct"), control);
updateMireds(value);
function cctInit(value) {
const mireds = document.getElementById("mireds");
if (mireds) {
mireds.setAttribute("min", value.cold);
mireds.setAttribute("max", value.warm);
}
}
function updateBrightness(value) {
let brightness = document.getElementById("brightness");
if (brightness !== null) {
brightness.value = value;
brightness.nextElementSibling.textContent = value;
return;
function updateLight(data) {
for (const [key, value] of Object.entries(data)) {
switch (key) {
case "state":
updateLightState(value);
break;
case "channels":
initLightState();
initBrightness();
initChannels(value);
addSimpleEnumerables("channel", "Channel", value);
break;
case "cct":
cctInit(value);
break;
case "brightness":
updateBrightness(value);
break;
case "values":
updateChannels(value);
break;
case "rgb":
case "hsv":
colorUpdate(key, value);
break;
case "mireds":
updateMireds(value);
break;
}
}
}
function onChannelSliderChange(event) {
event.target.nextElementSibling.textContent = event.target.value;
let template = loadTemplate("brightness-control");
let channel = {}
channel[event.target.dataset["id"]] = event.target.value;
let slider = template.getElementById("brightness");
slider.value = value;
slider.nextElementSibling.textContent = value;
sendAction("light", {channel});
}
function onBrightnessSliderChange(event) {
event.target.nextElementSibling.textContent = event.target.value;
sendAction("light", {brightness: event.target.value});
}
function initBrightness() {
const template = loadTemplate("brightness-control");
const slider = template.getElementById("brightness");
slider.addEventListener("change", onBrightnessSliderChange);
mergeTemplate(document.getElementById("light"), template);
}
function initChannels(container, channels) {
channels.forEach((value, channel) => {
let line = loadTemplate("channel-control");
function updateBrightness(value) {
const brightness = document.getElementById("brightness");
if (brightness !== null) {
brightness.value = value;
brightness.nextElementSibling.textContent = value;
}
}
function initChannels(channels) {
const container = document.getElementById("light");
for (let channel = 0; channel < channels; ++channel) {
const line = loadTemplate("channel-control");
line.querySelector("span.slider").dataset["id"] = channel;
let slider = line.querySelector("input.slider");
slider.value = value;
slider.nextElementSibling.textContent = value;
const slider = line.querySelector("input.slider");
slider.dataset["id"] = channel;
slider.addEventListener("change", onChannelSliderChange);
line.querySelector("label").textContent = "Channel #".concat(channel);
mergeTemplate(container, line);
});
}
}
function updateChannels(channels) {
let container = document.getElementById("channels");
if (container.childElementCount > 0) {
channels.forEach((value, channel) => {
let slider = container.querySelector(`input.slider[data-id='${channel}']`);
if (!slider) {
return;
}
// If there are RGB controls, no need for raw channel sliders
if (ColorPicker && (channel < 3)) {
slider.parentElement.style.display = "none";
}
// Or, when there are CCT controls
if ((channel === 3) || (channel === 4)) {
let cct = document.getElementById("cct");
if (cct.childElementCount > 0) {
slider.parentElement.style.display = "none";
}
}
slider.value = value;
slider.nextElementSibling.textContent = value;
});
function updateChannels(values) {
const container = document.getElementById("channels");
if (!container) {
return;
}
initChannels(container, channels);
updateChannels(channels);
values.forEach((value, channel) => {
const slider = container.querySelector(`input.slider[data-id='${channel}']`);
if (!slider) {
return;
}
slider.value = value;
slider.nextElementSibling.textContent = value;
});
}
//endRemoveIf(!light)
@ -2524,31 +2626,25 @@ function processData(data) {
//removeIf(!light)
if ("lightstate" === key) {
let color = document.getElementById("color");
color.style.display = value ? "inherit" : "none";
if ("light" === key) {
updateLight(value);
return;
}
if (("rgb" === key) || ("hsv" === key)) {
updateColor(key, value);
return;
if ("ltRelay" === key) {
showLightState(value);
}
if ("brightness" === key) {
updateBrightness(value);
return;
if ("useCCT" === key) {
initMireds(value);
}
if ("channels" === key) {
updateChannels(value);
addSimpleEnumerables("channel", "Channel", value.length);
return;
if ("useColor" === key) {
colorEnabled(value);
}
if ("mireds" === key) {
updateMireds(value);
return;
if ("useRGB" === key) {
colorInit(value ? "rgb" : "hsv");
}
//endRemoveIf(!light)


+ 8
- 8
code/html/index.html View File

@ -231,13 +231,13 @@
<div id="relays"></div>
<!-- removeIf(!light) -->
<div id="color"></div>
<div id="cct"></div>
<!-- endRemoveIf(!light) -->
<div id="light">
<div class="pure-control-group light-control">
<label>Lights</label>
<div><input type="checkbox" name="light-state"></div>
</div>
</div>
<!-- removeIf(!light) -->
<div id="channels"></div>
<div id="light"></div>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!sensor) -->
@ -2376,9 +2376,9 @@
<!-- removeIf(!light) -->
<template id="template-channel-control">
<div class="pure-control-group">
<div class="pure-control-group light-channel">
<label>Channel #</label>
<input type="range" min="0" max="255" class="slider channels pure-input-2-3">
<input type="range" min="0" max="255" class="slider pure-input-2-3">
<span class="slider"></span>
</div>
</template>


Loading…
Cancel
Save