Skip to content

Commit 79b4987

Browse files
nikita240CalcProgrammer1
authored andcommitted
Add Nanoleaf support
1 parent 7faa3b4 commit 79b4987

19 files changed

+2950
-32
lines changed

.gitlab-ci.yml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- shared-windows
1414
- windows
1515
- windows-1809
16-
16+
1717
stages:
1818
- build
1919
- test
@@ -37,7 +37,7 @@ before_script:
3737
script:
3838
- export $(dpkg-architecture)
3939
- ./scripts/build-appimage.sh
40-
40+
4141
artifacts:
4242
name: "${CI_PROJECT_NAME}_Linux_32_${CI_COMMIT_SHORT_SHA}"
4343
paths:
@@ -56,7 +56,7 @@ before_script:
5656
script:
5757
- export $(dpkg-architecture)
5858
- ./scripts/build-appimage.sh
59-
59+
6060
artifacts:
6161
name: "${CI_PROJECT_NAME}_Linux_64_${CI_COMMIT_SHORT_SHA}"
6262
paths:
@@ -183,8 +183,8 @@ before_script:
183183
image: fedora:36
184184
stage: build
185185
script:
186-
- dnf install rpmdevtools dnf-plugins-core -y
187-
- rpmdev-setuptree
186+
- dnf install rpmdevtools dnf-plugins-core libcurl-devel -y
187+
- rpmdev-setuptree
188188
- ls /root/
189189
- cp fedora/OpenRGB.spec /root/rpmbuild/SPECS
190190
- cp ../OpenRGB /root/rpmbuild/SOURCES/ -r
@@ -357,42 +357,42 @@ before_script:
357357
- '& cmd.exe /C "vcvarsall.bat x86 & set" | Foreach-Object { if ($_ -match "(.*?)=(.*)") { Set-Item -force -path "Env:\$($matches[1])" -value "$($matches[2])" } }'
358358
- Pop-Location
359359
- _fold_final_
360-
360+
361361
- _fold_start_ 'downloading precompiled versions of qtbase, qttools (for windeployqt) and jom (for a more parallel nmake)'
362362
- mkdir _qt
363363
- mkdir _qt_download
364364
- Push-Location _qt_download
365-
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win32_msvc2019/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86.7z'
365+
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win32_msvc2019/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86.7z'
366366
- curl.exe -LJ -o qt-tools.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win32_msvc2019/5.15.0-0-202005150700qttools-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86.7z'
367367
- curl.exe -LJ -o qt-jom.zip 'https://qt-mirror.dannhauer.de/official_releases/jom/jom.zip'
368368
- _fold_final_
369-
369+
370370
- _fold_start_ 'extracting the downloaded qt binaries'
371371
- 7z x qt-base.7z '-o../_qt' -y
372372
- 7z x qt-tools.7z '-o../_qt' -y
373373
- 7z x qt-jom.zip '-o../_qt' -y
374374
- _fold_final_
375-
375+
376376
- _fold_start_ 'turn the qt install from enterprise to foss; remove the licensing checks'
377377
- ${qconfig-pri-folder} = '..\_qt\5.15.0\msvc2019\mkspecs\qconfig.pri'
378378
- (Get-Content ${qconfig-pri-folder}).replace('QT_EDITION = Enterprise', 'QT_EDITION = OpenSource') | Set-Content ${qconfig-pri-folder}
379379
- (Get-Content ${qconfig-pri-folder}).replace('QT_LICHECK = licheck.exe', '') | Set-Content ${qconfig-pri-folder}
380380
- Pop-Location
381381
- _fold_final_
382-
382+
383383
- _fold_start_ 'run qmake and generate the msvc nmake makefile'
384384
- mkdir _build; cd _build
385385
- ..\_qt\5.15.0\msvc2019\bin\qmake ..\OpenRGB.pro
386386
- _fold_final_
387-
387+
388388
- _fold_start_ 'start the actual build with jom instead of nmake; for speed'
389389
- ..\_qt\jom
390390
- _fold_final_
391-
391+
392392
- _fold_start_ 'run windeployqt to automatically copy the needed dll files'
393393
- ..\_qt\5.15.0\msvc2019\bin\windeployqt --no-angle --no-translations --no-opengl-sw --no-system-d3d-compiler --no-compiler-runtime --no-webkit2 .\release\
394394
- _fold_final_
395-
395+
396396
- _fold_start_ 'Moving results for upload'
397397
- mv release ../'OpenRGB Windows 32-bit'
398398
- _fold_final_
@@ -426,42 +426,42 @@ before_script:
426426
- '& cmd.exe /C "vcvarsall.bat x64 & set" | Foreach-Object { if ($_ -match "(.*?)=(.*)") { Set-Item -force -path "Env:\$($matches[1])" -value "$($matches[2])" } }'
427427
- Pop-Location
428428
- _fold_final_
429-
429+
430430
- _fold_start_ 'downloading precompiled versions of qtbase, qttools (for windeployqt) and jom (for a more parallel nmake)'
431431
- mkdir _qt
432432
- mkdir _qt_download
433433
- Push-Location _qt_download
434-
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win64_msvc2019_64/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64.7z'
434+
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win64_msvc2019_64/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64.7z'
435435
- curl.exe -LJ -o qt-tools.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win64_msvc2019_64/5.15.0-0-202005150700qttools-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64.7z'
436436
- curl.exe -LJ -o qt-jom.zip 'https://qt-mirror.dannhauer.de/official_releases/jom/jom.zip'
437437
- _fold_final_
438-
438+
439439
- _fold_start_ 'extracting the downloaded qt binaries'
440440
- 7z x qt-base.7z '-o../_qt' -y
441441
- 7z x qt-tools.7z '-o../_qt' -y
442442
- 7z x qt-jom.zip '-o../_qt' -y
443443
- _fold_final_
444-
444+
445445
- _fold_start_ 'turn the qt install from enterprise to foss; remove the licensing checks'
446446
- ${qconfig-pri-folder} = '..\_qt\5.15.0\msvc2019_64\mkspecs\qconfig.pri'
447447
- (Get-Content ${qconfig-pri-folder}).replace('QT_EDITION = Enterprise', 'QT_EDITION = OpenSource') | Set-Content ${qconfig-pri-folder}
448448
- (Get-Content ${qconfig-pri-folder}).replace('QT_LICHECK = licheck.exe', '') | Set-Content ${qconfig-pri-folder}
449449
- Pop-Location
450450
- _fold_final_
451-
451+
452452
- _fold_start_ 'run qmake and generate the msvc nmake makefile'
453453
- mkdir _build; cd _build
454454
- ..\_qt\5.15.0\msvc2019_64\bin\qmake ..\OpenRGB.pro
455455
- _fold_final_
456-
456+
457457
- _fold_start_ 'start the actual build with jom instead of nmake; for speed'
458458
- ..\_qt\jom
459459
- _fold_final_
460-
460+
461461
- _fold_start_ 'run windeployqt to automatically copy the needed dll files'
462462
- ..\_qt\5.15.0\msvc2019_64\bin\windeployqt --no-angle --no-translations --no-opengl-sw --no-system-d3d-compiler --no-compiler-runtime --no-webkit2 .\release\
463463
- _fold_final_
464-
464+
465465
- _fold_start_ 'Moving results for upload'
466466
- mv release ../'OpenRGB Windows 64-bit'
467467
- _fold_final_
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*-----------------------------------------*\
2+
| NanoleafController.cpp |
3+
| |
4+
| API Interface for Nanoleaf devices |
5+
| |
6+
| Nikita Rushmanov 01/13/2022 |
7+
\*-----------------------------------------*/
8+
9+
#include "NanoleafController.h"
10+
#include "LogManager.h"
11+
#include <curl/curl.h>
12+
13+
std::size_t WriteMemoryCallback(const char* in, std::size_t size, std::size_t num, std::string* out)
14+
{
15+
const std::size_t totalBytes(size * num);
16+
out->append(in, totalBytes);
17+
return totalBytes;
18+
}
19+
20+
long APIRequest(std::string method, std::string location, std::string URI, json* request_data = nullptr, json* response_data = nullptr)
21+
{
22+
const std::string url("http://"+location+URI);
23+
24+
CURL* curl = curl_easy_init();
25+
26+
// Set remote URL.
27+
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str());
28+
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
29+
30+
// Don't bother trying IPv6, which would increase DNS resolution time.
31+
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
32+
33+
// Don't wait forever, time out after 10 seconds.
34+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
35+
36+
// Follow HTTP redirects if necessary.
37+
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
38+
39+
if(request_data)
40+
{
41+
// LOG_DEBUG("[Nanoleaf] Sending data: %s", request_data->dump().c_str());
42+
curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request_data->dump().c_str());
43+
}
44+
45+
// Response information.
46+
long httpCode(0);
47+
std::unique_ptr<std::string> httpData(new std::string());
48+
49+
// Hook up data handling function.
50+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
51+
52+
/*---------------------------------------------------------*\
53+
| Hook up data container (will be passed as the last |
54+
| parameter to the callback handling function). Can be any |
55+
| pointer type, since it will internally be passed as a |
56+
| void pointer. |
57+
\*---------------------------------------------------------*/
58+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, httpData.get());
59+
60+
// Run our HTTP GET command, capture the HTTP response code, and clean up.
61+
curl_easy_perform(curl);
62+
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
63+
curl_easy_cleanup(curl);
64+
65+
if (httpCode/100 == 2)
66+
{
67+
if(response_data)
68+
{
69+
*response_data = json::parse(*httpData.get());
70+
}
71+
}
72+
else
73+
{
74+
LOG_DEBUG("[Nanoleaf] HTTP %i:Could not %s from %s", httpCode, method, url);
75+
}
76+
77+
return httpCode;
78+
}
79+
80+
NanoleafController::NanoleafController(std::string a_address, int a_port, std::string a_auth_token)
81+
{
82+
address = a_address;
83+
port = a_port;
84+
auth_token = a_auth_token;
85+
location = address+":"+std::to_string(port);
86+
87+
json data;
88+
if(APIRequest("GET", location, "/api/v1/"+auth_token, nullptr, &data) == 200)
89+
{
90+
name = data["name"];
91+
serial = data["serialNo"];
92+
manufacturer = data["manufacturer"];
93+
firmware_version = data["firmwareVersion"];
94+
model = data["model"];
95+
96+
brightness = data["state"]["brightness"]["value"];
97+
selectedEffect = data["effects"]["select"];
98+
99+
for(json::const_iterator it = data["effects"]["effectsList"].begin(); it != data["effects"]["effectsList"].end(); ++it)
100+
{
101+
effects.push_back(it.value());
102+
}
103+
104+
for(json::const_iterator it = data["panelLayout"]["layout"]["positionData"].begin(); it != data["panelLayout"]["layout"]["positionData"].end(); ++it)
105+
{
106+
panel_ids.push_back(it.value()["panelId"].get<int>());
107+
}
108+
}
109+
else
110+
{
111+
throw std::exception();
112+
}
113+
}
114+
115+
std::string NanoleafController::Pair(std::string address, int port)
116+
{
117+
const std::string location = address+":"+std::to_string(port);
118+
119+
json data;
120+
if(APIRequest("POST", location, "/api/v1/new", nullptr, &data) == 200)
121+
{
122+
return data["auth_token"];
123+
}
124+
else
125+
{
126+
throw std::exception();
127+
}
128+
}
129+
130+
void NanoleafController::Unpair(std::string address, int port, std::string auth_token)
131+
{
132+
const std::string location = address+":"+std::to_string(port);
133+
134+
// We really don't care if this fails.
135+
APIRequest("DELETE", location, "/api/v1/"+auth_token, nullptr, nullptr);
136+
}
137+
138+
void NanoleafController::UpdateLEDs(std::vector<RGBColor>& colors)
139+
{
140+
// Requires StartExternalControl() to have been called prior.
141+
142+
if(model == NANOLEAF_LIGHT_PANELS_MODEL)
143+
{
144+
uint8_t size = panel_ids.size();
145+
146+
uint8_t* message = (uint8_t*)malloc(size*7+6+1);
147+
148+
message[0] = (uint8_t)size;
149+
150+
for (int i = 0; i < size; i++)
151+
{
152+
message[7*i+0+1] = (uint8_t)panel_ids[i];
153+
message[7*i+1+1] = (uint8_t)1;
154+
message[7*i+2+1] = (uint8_t)RGBGetRValue(colors[i]);
155+
message[7*i+3+1] = (uint8_t)RGBGetGValue(colors[i]);
156+
message[7*i+4+1] = (uint8_t)RGBGetBValue(colors[i]);
157+
message[7*i+5+1] = (uint8_t)0;
158+
message[7*i+6+1] = (uint8_t)0;
159+
}
160+
161+
external_control_socket.udp_write(reinterpret_cast<char*>(message), size*7+6+1);
162+
}
163+
else if(model == NANOLEAF_CANVAS_MODEL)
164+
{
165+
// Insert V2 protocol implementation here.
166+
}
167+
}
168+
169+
void NanoleafController::StartExternalControl()
170+
{
171+
json request;
172+
request["write"]["command"] = "display";
173+
request["write"]["animType"] = "extControl";
174+
175+
if(model == NANOLEAF_LIGHT_PANELS_MODEL)
176+
{
177+
request["write"]["extControlVersion"] = "v1";
178+
}
179+
else if(model == NANOLEAF_CANVAS_MODEL)
180+
{
181+
request["write"]["extControlVersion"] = "v2";
182+
}
183+
184+
json response;
185+
if(APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request, &response)/100 == 2)
186+
{
187+
external_control_socket.udp_client(response["streamControlIpAddr"].get<std::string>().c_str(), std::to_string(response["streamControlPort"].get<int>()).c_str());
188+
189+
selectedEffect = NANOLEAF_DIRECT_MODE_EFFECT_NAME;
190+
}
191+
}
192+
193+
void NanoleafController::SelectEffect(std::string effect_name)
194+
{
195+
json request;
196+
request["select"] = effect_name;
197+
if(APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request)/100 == 2)
198+
{
199+
selectedEffect = effect_name;
200+
}
201+
}
202+
203+
void NanoleafController::SetBrightness(int a_brightness)
204+
{
205+
json request;
206+
request["brightness"]["value"] = a_brightness;
207+
if(APIRequest("PUT", location, "/api/v1/"+auth_token+"/state", &request)/100 == 2)
208+
{
209+
brightness = a_brightness;
210+
}
211+
}

0 commit comments

Comments
 (0)