WLED/usermods/boblight/boblight.h

460 wiersze
16 KiB
C++

#pragma once
#include "wled.h"
/*
* Usermod that implements BobLight "ambilight" protocol
*
* See the accompanying README.md file for more info.
*/
#ifndef BOB_PORT
#define BOB_PORT 19333 // Default boblightd port
#endif
class BobLightUsermod : public Usermod {
typedef struct _LIGHT {
char lightname[5];
float hscan[2];
float vscan[2];
} light_t;
private:
unsigned long lastTime = 0;
bool enabled = false;
bool initDone = false;
light_t *lights = nullptr;
uint16_t numLights = 0; // 16 + 9 + 16 + 9
uint16_t top, bottom, left, right; // will be filled in readFromConfig()
uint16_t pct;
WiFiClient bobClient;
WiFiServer *bob;
uint16_t bobPort = BOB_PORT;
static const char _name[];
static const char _enabled[];
/*
# boblight
# Copyright (C) Bob 2009
#
# makeboblight.sh created by Adam Boeglin <adamrb@gmail.com>
#
# boblight is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# boblight is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// fills the lights[] array with position & depth of scan for each LED
void fillBobLights(int bottom, int left, int top, int right, float pct_scan) {
int lightcount = 0;
int total = top+left+right+bottom;
int bcount;
if (total > strip.getLengthTotal()) {
DEBUG_PRINTLN(F("BobLight: Too many lights."));
return;
}
// start left part of bottom strip (clockwise direction, 1st half)
if (bottom > 0) {
bcount = 1;
float brange = 100.0/bottom;
float bcurrent = 50.0;
if (bottom < top) {
int diff = top - bottom;
brange = 100.0/top;
bcurrent -= (diff/2)*brange;
}
while (bcount <= bottom/2) {
float btop = bcurrent - brange;
String name = "b"+String(bcount);
strncpy(lights[lightcount].lightname, name.c_str(), 4);
lights[lightcount].hscan[0] = btop;
lights[lightcount].hscan[1] = bcurrent;
lights[lightcount].vscan[0] = 100 - pct_scan;
lights[lightcount].vscan[1] = 100;
lightcount+=1;
bcurrent = btop;
bcount+=1;
}
}
// left side
if (left > 0) {
int lcount = 1;
float lrange = 100.0/left;
float lcurrent = 100.0;
while (lcount <= left) {
float ltop = lcurrent - lrange;
String name = "l"+String(lcount);
strncpy(lights[lightcount].lightname, name.c_str(), 4);
lights[lightcount].hscan[0] = 0;
lights[lightcount].hscan[1] = pct_scan;
lights[lightcount].vscan[0] = ltop;
lights[lightcount].vscan[1] = lcurrent;
lightcount+=1;
lcurrent = ltop;
lcount+=1;
}
}
// top side
if (top > 0) {
int tcount = 1;
float trange = 100.0/top;
float tcurrent = 0;
while (tcount <= top) {
float ttop = tcurrent + trange;
String name = "t"+String(tcount);
strncpy(lights[lightcount].lightname, name.c_str(), 4);
lights[lightcount].hscan[0] = tcurrent;
lights[lightcount].hscan[1] = ttop;
lights[lightcount].vscan[0] = 0;
lights[lightcount].vscan[1] = pct_scan;
lightcount+=1;
tcurrent = ttop;
tcount+=1;
}
}
// right side
if (right > 0) {
int rcount = 1;
float rrange = 100.0/right;
float rcurrent = 0;
while (rcount <= right) {
float rtop = rcurrent + rrange;
String name = "r"+String(rcount);
strncpy(lights[lightcount].lightname, name.c_str(), 4);
lights[lightcount].hscan[0] = 100-pct_scan;
lights[lightcount].hscan[1] = 100;
lights[lightcount].vscan[0] = rcurrent;
lights[lightcount].vscan[1] = rtop;
lightcount+=1;
rcurrent = rtop;
rcount+=1;
}
}
// right side of bottom strip (2nd half)
if (bottom > 0) {
float brange = 100.0/bottom;
float bcurrent = 100;
if (bottom < top) {
brange = 100.0/top;
}
while (bcount <= bottom) {
float btop = bcurrent - brange;
String name = "b"+String(bcount);
strncpy(lights[lightcount].lightname, name.c_str(), 4);
lights[lightcount].hscan[0] = btop;
lights[lightcount].hscan[1] = bcurrent;
lights[lightcount].vscan[0] = 100 - pct_scan;
lights[lightcount].vscan[1] = 100;
lightcount+=1;
bcurrent = btop;
bcount+=1;
}
}
numLights = lightcount;
#if WLED_DEBUG
DEBUG_PRINTLN(F("Fill light data: "));
DEBUG_PRINTF(" lights %d\n", numLights);
for (int i=0; i<numLights; i++) {
DEBUG_PRINTF(" light %s scan %2.1f %2.1f %2.1f %2.1f\n", lights[i].lightname, lights[i].vscan[0], lights[i].vscan[1], lights[i].hscan[0], lights[i].hscan[1]);
}
#endif
}
void BobSync() { yield(); } // allow other tasks, should also be used to force pixel redraw (not with WLED)
void BobClear() { for (size_t i=0; i<numLights; i++) setRealtimePixel(i, 0, 0, 0, 0); }
void pollBob();
public:
void setup() {
uint16_t totalLights = bottom + left + top + right;
if ( totalLights > strip.getLengthTotal() ) {
DEBUG_PRINTLN(F("BobLight: Too many lights."));
DEBUG_PRINTF("%d+%d+%d+%d>%d\n", bottom, left, top, right, strip.getLengthTotal());
totalLights = strip.getLengthTotal();
top = bottom = (uint16_t) roundf((float)totalLights * 16.0f / 50.0f);
left = right = (uint16_t) roundf((float)totalLights * 9.0f / 50.0f);
}
lights = new light_t[totalLights];
if (lights) fillBobLights(bottom, left, top, right, float(pct)); // will fill numLights
else enable(false);
initDone = true;
}
void connected() {
// we can only start server when WiFi is connected
if (!bob) bob = new WiFiServer(bobPort, 1);
bob->begin();
bob->setNoDelay(true);
}
void loop() {
if (!enabled || strip.isUpdating()) return;
if (millis() - lastTime > 10) {
lastTime = millis();
pollBob();
}
}
void enable(bool en) { enabled = en; }
#ifndef WLED_DISABLE_MQTT
/**
* handling of MQTT message
* topic only contains stripped topic (part after /wled/MAC)
* topic should look like: /swipe with amessage of [up|down]
*/
bool onMqttMessage(char* topic, char* payload) {
//if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/subtopic"), 6) == 0) {
// String action = payload;
// if (action == "on") {
// enable(true);
// return true;
// } else if (action == "off") {
// enable(false);
// return true;
// }
//}
return false;
}
/**
* subscribe to MQTT topic for controlling usermod
*/
void onMqttConnect(bool sessionPresent) {
//char subuf[64];
//if (mqttDeviceTopic[0] != 0) {
// strcpy(subuf, mqttDeviceTopic);
// strcat_P(subuf, PSTR("/subtopic"));
// mqtt->subscribe(subuf, 0);
//}
}
#endif
void addToJsonInfo(JsonObject& root)
{
JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject("u");
JsonArray infoArr = user.createNestedArray(FPSTR(_name));
String uiDomString = F("<button class=\"btn btn-xs\" onclick=\"requestJson({");
uiDomString += FPSTR(_name);
uiDomString += F(":{");
uiDomString += FPSTR(_enabled);
uiDomString += enabled ? F(":false}});\">") : F(":true}});\">");
uiDomString += F("<i class=\"icons ");
uiDomString += enabled ? "on" : "off";
uiDomString += F("\">&#xe08f;</i></button>");
infoArr.add(uiDomString);
}
/*
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void addToJsonState(JsonObject& root)
{
}
/*
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void readFromJsonState(JsonObject& root) {
if (!initDone) return; // prevent crash on boot applyPreset()
bool en = enabled;
JsonObject um = root[FPSTR(_name)];
if (!um.isNull()) {
if (um[FPSTR(_enabled)].is<bool>()) {
en = um[FPSTR(_enabled)].as<bool>();
} else {
String str = um[FPSTR(_enabled)]; // checkbox -> off or on
en = (bool)(str!="off"); // off is guaranteed to be present
}
if (en != enabled && lights) {
enable(en);
if (!enabled && bob && bob->hasClient()) {
if (bobClient) bobClient.stop();
bobClient = bob->available();
BobClear();
exitRealtime();
}
}
}
}
void appendConfigData() {
//oappend(SET_F("dd=addDropdown('usermod','selectfield');"));
//oappend(SET_F("addOption(dd,'1st value',0);"));
//oappend(SET_F("addOption(dd,'2nd value',1);"));
oappend(SET_F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field
oappend(SET_F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field
oappend(SET_F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field
oappend(SET_F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field
oappend(SET_F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field
}
void addToConfig(JsonObject& root) {
JsonObject umData = root.createNestedObject(FPSTR(_name));
umData[FPSTR(_enabled)] = enabled;
umData[F("port")] = bobPort;
umData[F("top")] = top;
umData[F("bottom")] = bottom;
umData[F("left")] = left;
umData[F("right")] = right;
umData[F("pct")] = pct;
}
bool readFromConfig(JsonObject& root) {
JsonObject umData = root[FPSTR(_name)];
bool configComplete = !umData.isNull();
bool en = enabled;
configComplete &= getJsonValue(umData[FPSTR(_enabled)], en);
enable(en);
configComplete &= getJsonValue(umData[F("port")], bobPort);
configComplete &= getJsonValue(umData[F("bottom")], bottom, 16);
configComplete &= getJsonValue(umData[F("top")], top, 16);
configComplete &= getJsonValue(umData[F("left")], left, 9);
configComplete &= getJsonValue(umData[F("right")], right, 9);
configComplete &= getJsonValue(umData[F("pct")], pct, 5); // Depth of scan [%]
pct = MIN(50,MAX(1,pct));
uint16_t totalLights = bottom + left + top + right;
if (initDone && numLights != totalLights) {
if (lights) delete[] lights;
setup();
}
return configComplete;
}
/*
* handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.
* Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.
* Commonly used for custom clocks (Cronixie, 7 segment)
*/
void handleOverlayDraw() {
//strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black
}
uint16_t getId() { return USERMOD_ID_BOBLIGHT; }
};
// strings to reduce flash memory usage (used more than twice)
const char BobLightUsermod::_name[] PROGMEM = "BobLight";
const char BobLightUsermod::_enabled[] PROGMEM = "enabled";
// main boblight handling (definition here prevents inlining)
void BobLightUsermod::pollBob() {
//check if there are any new clients
if (bob && bob->hasClient()) {
//find free/disconnected spot
if (!bobClient || !bobClient.connected()) {
if (bobClient) bobClient.stop();
bobClient = bob->available();
DEBUG_PRINTLN(F("Boblight: Client connected."));
}
//no free/disconnected spot so reject
WiFiClient bobClientTmp = bob->available();
bobClientTmp.stop();
BobClear();
exitRealtime();
}
//check clients for data
if (bobClient && bobClient.connected()) {
realtimeLock(realtimeTimeoutMs); // lock strip as we have a client connected
//get data from the client
while (bobClient.available()) {
String input = bobClient.readStringUntil('\n');
// DEBUG_PRINT("Client: "); DEBUG_PRINTLN(input); // may be to stressful on Serial
if (input.startsWith(F("hello"))) {
DEBUG_PRINTLN(F("hello"));
bobClient.print(F("hello\n"));
} else if (input.startsWith(F("ping"))) {
DEBUG_PRINTLN(F("ping 1"));
bobClient.print(F("ping 1\n"));
} else if (input.startsWith(F("get version"))) {
DEBUG_PRINTLN(F("version 5"));
bobClient.print(F("version 5\n"));
} else if (input.startsWith(F("get lights"))) {
char tmp[64];
String answer = "";
sprintf_P(tmp, PSTR("lights %d\n"), numLights);
DEBUG_PRINT(tmp);
answer.concat(tmp);
for (int i=0; i<numLights; i++) {
sprintf_P(tmp, PSTR("light %s scan %2.1f %2.1f %2.1f %2.1f\n"), lights[i].lightname, lights[i].vscan[0], lights[i].vscan[1], lights[i].hscan[0], lights[i].hscan[1]);
DEBUG_PRINT(tmp);
answer.concat(tmp);
}
bobClient.print(answer);
} else if (input.startsWith(F("set priority"))) {
DEBUG_PRINTLN(F("set priority not implemented"));
// not implemented
} else if (input.startsWith(F("set light "))) { // <id> <cmd in rgb, speed, interpolation> <value> ...
input.remove(0,10);
String tmp = input.substring(0,input.indexOf(' '));
int light_id = -1;
for (uint16_t i=0; i<numLights; i++) {
if (strncmp(lights[i].lightname, tmp.c_str(), 4) == 0) {
light_id = i;
break;
}
}
if (light_id == -1) return;
input.remove(0,input.indexOf(' ')+1);
if (input.startsWith(F("rgb "))) {
input.remove(0,4);
tmp = input.substring(0,input.indexOf(' '));
uint8_t red = (uint8_t)(255.0f*tmp.toFloat());
input.remove(0,input.indexOf(' ')+1); // remove first float value
tmp = input.substring(0,input.indexOf(' '));
uint8_t green = (uint8_t)(255.0f*tmp.toFloat());
input.remove(0,input.indexOf(' ')+1); // remove second float value
tmp = input.substring(0,input.indexOf(' '));
uint8_t blue = (uint8_t)(255.0f*tmp.toFloat());
//strip.setPixelColor(light_id, RGBW32(red, green, blue, 0));
setRealtimePixel(light_id, red, green, blue, 0);
} // currently no support for interpolation or speed, we just ignore this
} else if (input.startsWith(F("sync"))) {
BobSync();
} else {
// Client sent gibberish
DEBUG_PRINTLN(F("Client sent gibberish."));
bobClient.stop();
bobClient = bob->available();
BobClear();
}
}
}
}