lumini_p30_control/firmware/firmware.ino

639 lines
21 KiB
Arduino
Raw Normal View History

2023-04-24 20:26:49 +02:00
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
#include "config.h"
//********* Config block *********//
uint8_t pins[LIGHTS_COUNT] = {12, 15, 13, 14};
IPAddress strip_ip ( 192, 168, 0, 26); // choose an unique IP Adress
IPAddress gateway_ip ( 192, 168, 0, 1); // Router IP
IPAddress subnet_mask( 255, 255, 255, 0);
//********************************//
#define LIGHT_VERSION 2.1
#define LAST_STATE_STARTUP_LIGHT_LAST_STATE 0
#define LAST_STATE_STARTUP_LIGHT_ON_STATE 1
#define LAST_STATE_STARTUP_LIGHT_OFF_STATE 2
#define LIGHT_STATE_ON 1
#define LIGHT_STATE_OFF 0
#define TIMING_CONTROL_ENABLED 1
#define TIMING_CONTROL_DISABLED 0
#define SCENE_RELEAX 0
#define SCENE_BRIGHT 1
#define SCENE_NIGHTLY 2
// 10 bit PWM
#define PWM_OFF 0 // 0V
#define PWM_MIN 640 // 15V - minimum light amount (~1%)
#define PWM_MAX 1023 // 24V - maximum light amount (100%)
#define PWM_INC 4 // 24V-15V = 9V range; 9V ≙ 1024/640 = 383 counts; 383/100% = 3,83 counts (1%) / % => round up 4 counts / % (~1%)
//********************************//
2023-04-24 20:26:49 +02:00
uint8_t scene;
uint8_t tc_enabled;
bool light_state[LIGHTS_COUNT];
bool in_transition;
int transitiontime[LIGHTS_COUNT];
int bri[LIGHTS_COUNT];
float step_level[LIGHTS_COUNT];
float current_bri[LIGHTS_COUNT];
byte mac[6];
ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdateServer;
//********************************//
2023-04-24 20:26:49 +02:00
void apply_scene(uint8_t new_scene, uint8_t light)
{
if (new_scene == SCENE_RELEAX)
{
bri[light] = 144;
} else if (new_scene == SCENE_BRIGHT)
{
bri[light] = 254;
} else if (new_scene == SCENE_NIGHTLY)
{
bri[0] = 25;
bri[1] = 0;
bri[2] = 0;
bri[3] = 0;
}
}
//********************************//
2023-04-24 20:26:49 +02:00
void process_lightdata(uint8_t light, float transitiontime)
{
transitiontime *= 16;
if (light_state[light])
{
step_level[light] = (bri[light] - current_bri[light]) / transitiontime;
} else {
step_level[light] = current_bri[light] / transitiontime;
}
}
//********************************//
2023-04-24 20:26:49 +02:00
void lightEngine()
{
for (int i = 0; i < LIGHTS_COUNT; i++)
{
if (light_state[i])
{
if (bri[i] != current_bri[i])
{
in_transition = true;
current_bri[i] += step_level[i];
if ((step_level[i] > 0.0 && current_bri[i] > bri[i]) || (step_level[i] < 0.0 && current_bri[i] < bri[i]))
{
current_bri[i] = bri[i];
}
analogWrite(pins[i], (int)(current_bri[i] * 4)); // TODO warum * 4?
2023-04-24 20:26:49 +02:00
}
} else {
if (current_bri[i] != 0 )
{
in_transition = true;
current_bri[i] -= step_level[i];
if (current_bri[i] < 0)
{
current_bri[i] = 0;
}
analogWrite(pins[i], (int)(current_bri[i] * 4)); // TODO warum * 4?
2023-04-24 20:26:49 +02:00
}
}
}
if (in_transition)
{
delay(6);
in_transition = false;
}
}
//********************************//
2023-04-24 20:26:49 +02:00
void read_eeprom_config()
{
2023-04-24 20:26:49 +02:00
for (uint8_t light = 0; light < LIGHTS_COUNT; light++)
{
apply_scene(EEPROM.read(EEPROM_SCENE_ADDRESS), light);
step_level[light] = bri[light] / 150.0;
if (EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS) == LAST_STATE_STARTUP_LIGHT_LAST_STATE ||
(EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS) == LAST_STATE_STARTUP_LIGHT_ON_STATE &&
EEPROM.read(EEPROM_LAST_STATE_ADDRESS + light) == LIGHT_STATE_ON))
{
light_state[light] = true; // set the light state to on
}
}
uint8_t tmp = EEPROM.read(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS);
if (tmp == TIMING_CONTROL_DISABLED)
2023-04-24 20:26:49 +02:00
{
tc_enabled = TIMING_CONTROL_DISABLED;
} else if (tmp == TIMING_CONTROL_ENABLED)
2023-04-24 20:26:49 +02:00
{
tc_enabled = TIMING_CONTROL_ENABLED;
} else {
EEPROM.write(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS, TIMING_CONTROL_DISABLED);
EEPROM.commit();
tc_enabled = TIMING_CONTROL_DISABLED;
}
if (EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS) == 255)
{
// set the default value on uninitialized EEPROM
EEPROM.write(EEPROM_LAST_STATE_STARTUP_ADDRESS, 0);
EEPROM.commit();
}
#ifdef USE_STATIC_IP
if (EEPROM.read(EEPROM_DYNAMIC_IP_ADDRESS) == 255)
{
EEPROM.write(EEPROM_DYNAMIC_IP_ADDRESS, 0);
EEPROM.commit();
}
#else
if (EEPROM.read(EEPROM_DYNAMIC_IP_ADDRESS) == 255)
{
EEPROM.write(EEPROM_DYNAMIC_IP_ADDRESS, 1);
EEPROM.commit();
}
#endif
}
//********************************//
void setup()
{
EEPROM.begin(512);
Serial.begin(SERIAL_BAUD_RATE);
if (EEPROM.read(EEPROM_DYNAMIC_IP_ADDRESS) == 0)
{
WiFi.config(strip_ip, gateway_ip, subnet_mask);
}
read_eeprom_config();
2023-04-24 20:26:49 +02:00
for (int j = 0; j < 200; j++)
{
lightEngine();
}
WiFi.mode(WIFI_STA);
WiFiManager wifiManager;
wifiManager.setConfigPortalTimeout(120);
wifiManager.autoConnect(light_name);
IPAddress myIP = WiFi.localIP();
Serial.print("IP: ");
Serial.println(myIP);
if (!light_state[0])
{
// Show that we are connected
analogWrite(pins[0], 100);
delay(500);
analogWrite(pins[0], 0);
}
WiFi.macAddress(mac);
pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output
digitalWrite(LED_BUILTIN, HIGH); // Turn the LED off by making the voltage HIGH
httpUpdateServer.setup(&server); // start http server
init_webserver();
tc_init();
server.begin();
} // end of setup
void loop()
{
server.handleClient();
lightEngine();
if (tc_enabled == TIMING_CONTROL_ENABLED)
2023-04-24 20:26:49 +02:00
{
tc_update();
2023-04-24 20:26:49 +02:00
}
}
void handleNotFound()
{
String message = "File Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint8_t i = 0; i < server.args(); i++)
{
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", message);
}
2023-04-24 20:26:49 +02:00
void init_webserver()
{
server.on("/state", HTTP_PUT, []()
2023-04-24 20:26:49 +02:00
{ // HTTP PUT request used to set a new light state
DynamicJsonDocument root(1024);
DeserializationError error = deserializeJson(root, server.arg("plain"));
if (error)
{
server.send(404, "text/plain", "FAIL. " + server.arg("plain"));
} else {
for (JsonPair state : root.as<JsonObject>())
{
const char* key = state.key().c_str();
int light = atoi(key) - 1;
JsonObject values = state.value();
int transitiontime = 4;
uint8_t tmp = EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS);
2023-04-24 20:26:49 +02:00
if (values.containsKey("on"))
{
if (values["on"])
{
light_state[light] = true;
if (tmp == LAST_STATE_STARTUP_LIGHT_LAST_STATE &&
EEPROM.read(EEPROM_LAST_STATE_ADDRESS + light) == LIGHT_STATE_OFF)
2023-04-24 20:26:49 +02:00
{
EEPROM.write(EEPROM_LAST_STATE_ADDRESS + light, LIGHT_STATE_ON);
}
} else {
light_state[light] = false;
if (tmp == LAST_STATE_STARTUP_LIGHT_LAST_STATE &&
EEPROM.read(EEPROM_LAST_STATE_ADDRESS + light) == LIGHT_STATE_ON)
2023-04-24 20:26:49 +02:00
{
EEPROM.write(EEPROM_LAST_STATE_ADDRESS + light, LIGHT_STATE_OFF);
}
}
}
if (values.containsKey("bri"))
{
bri[light] = values["bri"];
}
if (values.containsKey("bri_inc"))
{
bri[light] += (int) values["bri_inc"];
if (bri[light] > 255) bri[light] = 255;
else if (bri[light] < 1) bri[light] = 1;
}
if (values.containsKey("transitiontime"))
{
transitiontime = values["transitiontime"];
}
process_lightdata(light, transitiontime);
}
String output;
serializeJson(root, output);
server.send(200, "text/plain", output);
}
});
server.on("/state", HTTP_GET, []()
{ // HTTP GET request used to fetch current light state
uint8_t light = server.arg("light").toInt() - 1;
DynamicJsonDocument root(1024);
root["on"] = light_state[light];
root["bri"] = bri[light];
String output;
serializeJson(root, output);
server.send(200, "text/plain", output);
});
server.on("/detect", []()
{ // HTTP GET request used to discover the light type
char macString[32] = {0};
sprintf(macString, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
DynamicJsonDocument root(1024);
root["name"] = light_name;
root["lights"] = LIGHTS_COUNT;
root["protocol"] = "native_multi";
root["modelid"] = "LWB010";
root["type"] = "dimmable_light";
root["mac"] = String(macString);
root["version"] = LIGHT_VERSION;
String output;
serializeJson(root, output);
server.send(200, "text/plain", output);
});
server.on("/", []()
{
static float transitiontime = 4.0;
if (server.hasArg("transition"))
{
transitiontime = server.arg("transition").toFloat();
}
2023-04-24 20:26:49 +02:00
// startup behavior switch handling
2023-04-24 20:26:49 +02:00
if (server.hasArg("startup"))
{
int startup = server.arg("startup").toInt();
if (EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS) != startup)
{
EEPROM.write(EEPROM_LAST_STATE_STARTUP_ADDRESS, startup);
EEPROM.commit();
Serial.print("Startup behavior set to "); Serial.println(EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS));
}
2023-04-24 20:26:49 +02:00
}
// timing controller switch handling
2023-04-24 20:26:49 +02:00
if (server.hasArg("tc"))
{
if (server.arg("tc") == "true")
{
if (tc_enabled == TIMING_CONTROL_DISABLED)
{
if (EEPROM.read(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS) != TIMING_CONTROL_ENABLED)
{
tc_enabled = TIMING_CONTROL_ENABLED;
EEPROM.write(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS, TIMING_CONTROL_ENABLED);
EEPROM.commit();
Serial.print("Timing control = "); Serial.println(EEPROM.read(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS));
}
2023-04-24 20:26:49 +02:00
}
} else { // tc is set to false or something else
if (tc_enabled == TIMING_CONTROL_ENABLED)
{
tc_enabled = TIMING_CONTROL_DISABLED;
if (EEPROM.read(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS) != TIMING_CONTROL_DISABLED)
{
EEPROM.write(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS, TIMING_CONTROL_DISABLED);
EEPROM.commit();
Serial.print("Timing control = "); Serial.println(EEPROM.read(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS));
}
2023-04-24 20:26:49 +02:00
}
}
}
// scene switch handling
2023-04-24 20:26:49 +02:00
if (server.hasArg("scene"))
{
scene = server.arg("scene").toInt();
if (EEPROM.read(EEPROM_SCENE_ADDRESS) != scene)
{
EEPROM.write(EEPROM_SCENE_ADDRESS, scene);
EEPROM.commit();
Serial.print("Scene set to "); Serial.println(EEPROM.read(EEPROM_SCENE_ADDRESS));
}
2023-04-24 20:26:49 +02:00
}
// process the received data for every light
2023-04-24 20:26:49 +02:00
for (int light = 0; light < LIGHTS_COUNT; light++)
{
if (server.hasArg("bri" + (String)light))
{
bri[light] = server.arg("bri" + (String)light).toInt();
Serial.print("Brightness set to "); Serial.println(bri[light]);
}
if (server.hasArg("on" + (String)light))
{
uint8_t tmp = EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS);
2023-04-24 20:26:49 +02:00
if (server.arg("on" + (String)light) == "true")
{
light_state[light] = true;
if (tmp == 0 && EEPROM.read(EEPROM_LAST_STATE_ADDRESS + light) == 0)
2023-04-24 20:26:49 +02:00
{
EEPROM.write(EEPROM_LAST_STATE_ADDRESS + light, LIGHT_STATE_ON);
}
} else {
light_state[light] = false;
if (tmp == 0 && EEPROM.read(EEPROM_LAST_STATE_ADDRESS + light) == 1)
2023-04-24 20:26:49 +02:00
{
EEPROM.write(EEPROM_LAST_STATE_ADDRESS + light, LIGHT_STATE_OFF);
}
}
Serial.print("Light "); Serial.print(light); Serial.print(" state set to "); Serial.println(light_state[light]);
EEPROM.commit();
}
// start alerting for every light
if (server.hasArg("alert"))
{
2023-04-24 20:26:49 +02:00
if (light_state[light])
{
current_bri[light] = 0;
} else {
current_bri[light] = 255;
}
}
// switch the light
2023-04-24 20:26:49 +02:00
if (light_state[light])
{
step_level[light] = ((float)bri[light] - current_bri[light]) / transitiontime;
} else {
step_level[light] = current_bri[light] / transitiontime;
}
}
if (server.hasArg("resettc"))
{ // reqrite the tc config and reboot
2023-04-24 20:26:49 +02:00
tc_write_default();
ESP.reset();
}
if (server.hasArg("reset"))
{
ESP.reset();
}
// Generate HTML page
2023-04-24 20:26:49 +02:00
String http_content = "<!doctype html>";
http_content += "<html>";
http_content += "<head>";
http_content += "<meta charset=\"utf-8\">";
http_content += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
http_content += "<meta http-equiv=\"refresh\" content=\"15\">"; // Reload the page every 15 seconds automatically
http_content += "<title>Light Setup</title>";
http_content += "<link rel=\"stylesheet\" href=\"https://unpkg.com/purecss@0.6.2/build/pure-min.css\">";
http_content += "</head>";
http_content += "<body>";
http_content += "<fieldset>";
http_content += "<h3>Setup of " + (String)LIGHTS_COUNT + " light"; if (LIGHTS_COUNT > 1) http_content += "'s"; http_content += "</h3>";
http_content += "<form class=\"pure-form pure-form-aligned\" action=\"/\" method=\"post\">";
// timer data processing, startup state and scene for all of the lights
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"startup\">Startup</label>";
http_content += "<select onchange=\"this.form.submit()\" id=\"startup\" name=\"startup\">";
int ls_val = EEPROM.read(EEPROM_LAST_STATE_STARTUP_ADDRESS);
http_content += "<option "; if (ls_val == LAST_STATE_STARTUP_LIGHT_LAST_STATE) http_content += "selected=\"selected\""; http_content += " value=\"0\">Last state</option>";
http_content += "<option "; if (ls_val == LAST_STATE_STARTUP_LIGHT_ON_STATE) http_content += "selected=\"selected\""; http_content += " value=\"1\">On</option>";
http_content += "<option "; if (ls_val == LAST_STATE_STARTUP_LIGHT_OFF_STATE) http_content += "selected=\"selected\""; http_content += " value=\"2\">Off</option>";
http_content += "</select>";
http_content += "</div>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"scene\">Scene</label>";
http_content += "<select onchange = \"this.form.submit()\" id=\"scene\" name=\"scene\">";
int sc_val = EEPROM.read(EEPROM_SCENE_ADDRESS);
http_content += "<option "; if (sc_val == SCENE_RELEAX) http_content += "selected=\"selected\""; http_content += " value=\"0\">Relax</option>";
http_content += "<option "; if (sc_val == SCENE_BRIGHT) http_content += "selected=\"selected\""; http_content += " value=\"1\">Bright</option>";
http_content += "<option "; if (sc_val == SCENE_NIGHTLY) http_content += "selected=\"selected\""; http_content += " value=\"2\">Night</option>";
http_content += "</select>";
http_content += "</div>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"power\"><strong>Enable timing control</strong></label>";
int tc_val = EEPROM.read(EEPROM_TIMING_CONTROL_ENABLED_ADDRESS);
http_content += "<a class=\"pure-button"; if (tc_val == TIMING_CONTROL_ENABLED) http_content += " pure-button-primary"; http_content += "\" href=\"/?tc=true\">ON</a>";
http_content += "<a class=\"pure-button"; if (tc_val == TIMING_CONTROL_DISABLED) http_content += " pure-button-primary"; http_content += "\" href=\"/?tc=false\">OFF</a>";
http_content += "</div>";
http_content += "<br>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"transition\">Transition time (s)</label>";
http_content += "<input id=\"transition\" name=\"transition\" type=\"text\" placeholder=\"10\" value\"" + (String)transitiontime + "\">";
http_content += "</div>";
2023-04-24 20:26:49 +02:00
// Light control
2023-04-24 20:26:49 +02:00
for (uint8 light_num = 0; light_num < LIGHTS_COUNT; light_num++)
{
http_content += "<h3>Light " + (String)(light_num+1) + "</h3>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"power\"><strong>Power</strong></label>";
http_content += "<a class=\"pure-button"; if ( light_state[light_num]) http_content += " pure-button-primary"; http_content += "\" href=\"/?on" + (String)light_num + "=true\">ON</a>";
http_content += "<a class=\"pure-button"; if (!light_state[light_num]) http_content += " pure-button-primary"; http_content += "\" href=\"/?on" + (String)light_num + "=false\">OFF</a>";
http_content += "</div>";
2023-04-24 20:26:49 +02:00
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"state\"><strong>State</strong></label>";
http_content += "</div>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"bri\"" + (String)light_num + ">Bri</label>";
http_content += "<input id=\"bri" + (String)light_num + "\" name=\"bri" + (String)light_num + "\" type=\"range\" min=\"0\" max=\"100\" value=\"" + (String)bri[light_num] + "\">";
http_content += "<label id=\"bri" + (String)light_num + "_val\" name=\"bri" + (String)light_num + "\">" + (String)bri[light_num] + "</label>";
http_content += "<script>";
http_content += "var slider" + (String)light_num + " = document.getElementById(\"bri" + (String)light_num + "\");";
http_content += "var output" + (String)light_num + " = document.getElementById(\"bri" + (String)light_num + "\_val\");";
http_content += "output" + (String)light_num + ".innerHTML = slider" + (String)light_num + ".value;";
http_content += "slider" + (String)light_num + ".oninput = function() {";
http_content += "output" + (String)light_num + ".innerHTML = this.value;";
http_content += "}";
http_content += "</script>";
http_content += "</div>";
}
// Wifi settings
http_content += "<br>";
http_content += "<h3>Wifi</h3>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"ip\">SSID</label>";
http_content += "<input id=\"ssid\" name=\"ssid\" type=\"text\" value=\"" + WiFi.SSID() + "\">";
http_content += "</div>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"wpw\">Passphrase</label>";
http_content += "<input id=\"wpw\" name=\"wpw\" type=\"text\" placeholder=\"1234password\">";
http_content += "</div>";
// Network settings
uint8_t dip = EEPROM.read(EEPROM_DYNAMIC_IP_ADDRESS);
http_content += "<br>";
http_content += "<h3>Network</h3>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"dip\"><strong>Dynamic-IP</strong></label>";
http_content += "<a class=\"pure-button"; if ( dip) http_content += " pure-button-primary"; http_content += "\" href=\"/?dip=true\">ON</a>";
http_content += "<a class=\"pure-button"; if (!dip) http_content += " pure-button-primary"; http_content += "\" href=\"/?dip=false\">OFF</a>";
http_content += "</div>";
if (dip == 0)
{
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"ip\">IP</label>";
http_content += "<input id=\"ip\" name=\"ip\" type=\"text\" value=\"" + WiFi.localIP().toString() + "\">";
http_content += "</div>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"gwip\">Gateway IP</label>";
http_content += "<input id=\"gwip\" name=\"gwip\" type=\"text\" value=\"" + WiFi.gatewayIP().toString() + "\">";
http_content += "</div>";
http_content += "<div class=\"pure-control-group\">";
http_content += "<label for=\"ip\">Netmask</label>";
http_content += "<input id=\"netmask\" name=\"netmas\" type=\"text\" value=\"" + WiFi.subnetMask().toString() + "\">";
http_content += "</div>";
}
http_content += "<div class=\"pure-controls\">";
http_content += "<span class=\"pure-form-message\"><a href=\"/?alert=1\">alert</a> &nbsp; <a href=\"/?reset=1\">reset</a> &nbsp; <a href=\"/?resettc\">reset timing control data</a> &nbsp; <a href=\"/update\">update</a></span>";
http_content += "<label for=\"cb\" class=\"pure-checkbox\">";
http_content += "</label>";
http_content += "<button type=\"submit\" class=\"pure-button pure-button-primary\">Save</button>";
http_content += "</div>";
http_content += "</fieldset>";
http_content += "</form>";
http_content += "</body>";
http_content += "</html>";
server.send(200, "text/html", http_content);
});
server.on("/reset", []()
{ // trigger manual reset
server.send(200, "text/html", "reset");
delay(1000);
ESP.restart();
});
server.onNotFound(handleNotFound);
}