b24c8b3410
* BobLight ambilight protocol implementation for WLED * Added usermod ID * Add realtime locking Add port config Bugfixes * Minor optimisation. * Fix WiFiServer start. * Bugfix * Working boblight. * npm run * Add readme * Undo PIR sensor modification. Undo npm run build. * Fix parentheses. * Comments. Cancel realtime when disabled.
458 lines
16 KiB
C++
458 lines
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; }
|
|
|
|
/**
|
|
* 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);
|
|
//}
|
|
}
|
|
|
|
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("\"></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();
|
|
}
|
|
}
|
|
}
|
|
}
|