/*-----------------------------------------*\ | NanoleafController.cpp | | | | API Interface for Nanoleaf devices | | | | Nikita Rushmanov 01/13/2022 | \*-----------------------------------------*/ #include "NanoleafController.h" #include "LogManager.h" #include std::size_t WriteMemoryCallback(const char* in, std::size_t size, std::size_t num, std::string* out) { const std::size_t totalBytes(size * num); out->append(in, totalBytes); return totalBytes; } long APIRequest(std::string method, std::string location, std::string URI, json* request_data = nullptr, json* response_data = nullptr) { const std::string url("http://"+location+URI); CURL* curl = curl_easy_init(); /*-------------------------------------------------------------*\ | Set remote URL. | \*-------------------------------------------------------------*/ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); /*-------------------------------------------------------------*\ | Don't bother trying IPv6, which would increase DNS resolution | | time. | \*-------------------------------------------------------------*/ curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); /*-------------------------------------------------------------*\ | Don't wait forever, time out after 10 seconds. | \*-------------------------------------------------------------*/ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10); /*-------------------------------------------------------------*\ | Follow HTTP redirects if necessary. | \*-------------------------------------------------------------*/ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); if(request_data) { curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request_data->dump().c_str()); } /*-------------------------------------------------------------*\ | Response information. | \*-------------------------------------------------------------*/ long httpCode(0); std::unique_ptr httpData(new std::string()); /*-------------------------------------------------------------*\ | Hook up data handling function. | \*-------------------------------------------------------------*/ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); /*-------------------------------------------------------------*\ | Hook up data container (will be passed as the last parameter | | to the callback handling function). Can be any pointer type, | | since it will internally be passed as a void pointer. | \*-------------------------------------------------------------*/ curl_easy_setopt(curl, CURLOPT_WRITEDATA, httpData.get()); /*-------------------------------------------------------------*\ | Run our HTTP GET command, capture the HTTP response code, and | | clean up. | \*-------------------------------------------------------------*/ curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); if((httpCode / 100) == 2) { if(response_data) { *response_data = json::parse(*httpData.get()); } } else { LOG_DEBUG("[Nanoleaf] HTTP %i:Could not %s from %s", httpCode, method, url); } return httpCode; } NanoleafController::NanoleafController(std::string a_address, int a_port, std::string a_auth_token) { address = a_address; port = a_port; auth_token = a_auth_token; location = address + ":" + std::to_string(port); json data; if(APIRequest("GET", location, "/api/v1/"+auth_token, nullptr, &data) == 200) { name = data["name"]; serial = data["serialNo"]; manufacturer = data["manufacturer"]; firmware_version = data["firmwareVersion"]; model = data["model"]; brightness = data["state"]["brightness"]["value"]; selectedEffect = data["effects"]["select"]; for(json::const_iterator it = data["effects"]["effectsList"].begin(); it != data["effects"]["effectsList"].end(); ++it) { effects.push_back(it.value()); } for(json::const_iterator it = data["panelLayout"]["layout"]["positionData"].begin(); it != data["panelLayout"]["layout"]["positionData"].end(); ++it) { panel_ids.push_back(it.value()["panelId"].get()); } } else { throw std::exception(); } } std::string NanoleafController::Pair(std::string address, int port) { const std::string location = address+":"+std::to_string(port); json data; if(APIRequest("POST", location, "/api/v1/new", nullptr, &data) == 200) { return data["auth_token"]; } else { throw std::exception(); } } void NanoleafController::Unpair(std::string address, int port, std::string auth_token) { const std::string location = address+":"+std::to_string(port); /*-------------------------------------------------------------*\ | We really don't care if this fails. | \*-------------------------------------------------------------*/ APIRequest("DELETE", location, "/api/v1/"+auth_token, nullptr, nullptr); } void NanoleafController::UpdateLEDs(std::vector& colors) { /*-------------------------------------------------------------*\ | Requires StartExternalControl() to have been called prior. | \*-------------------------------------------------------------*/ if(model == NANOLEAF_LIGHT_PANELS_MODEL) { /*---------------------------------------------------------*\ | Protocol V1 - https://forum.nanoleaf.me/docs | | | | Size Description | | --------------------------------------------------------- | | 1 nPanels Number of panels | | | | 1 panelId ID of panel | | 1 nFrames Number of frames (always 1) | | 1 R Red channel | | 1 G Green channel | | 1 B Blue channel | | 1 W White channel (ignored) | | 1 transitionTime Transition time (x 100ms) | \*---------------------------------------------------------*/ uint8_t size = panel_ids.size(); uint8_t* message = (uint8_t*)malloc((size * 7) + 1); message[0] = (uint8_t)size; /* nPanels */ for(int i = 0; i < size; i++) { message[(7 * i) + 0 + 1] = (uint8_t)panel_ids[i]; /* panelId */ message[(7 * i) + 1 + 1] = (uint8_t)1; /* nFrames */ message[(7 * i) + 2 + 1] = (uint8_t)RGBGetRValue(colors[i]); /* R */ message[(7 * i) + 3 + 1] = (uint8_t)RGBGetGValue(colors[i]); /* G */ message[(7 * i) + 4 + 1] = (uint8_t)RGBGetBValue(colors[i]); /* B */ message[(7 * i) + 5 + 1] = (uint8_t)0; /* W */ message[(7 * i) + 6 + 1] = (uint8_t)0; /* transitionTime */ } external_control_socket.udp_write((char*)message, (size * 7) + 1); free(message); } else if((model == NANOLEAF_CANVAS_MODEL) || (model == NANOLEAF_SHAPES_MODEL)) { /*---------------------------------------------------------*\ | Protocol V2 - https://forum.nanoleaf.me/docs | | | | Size Description | | --------------------------------------------------------- | | 2 nPanels Number of panels | | | | 2 panelId ID of panel | | 1 R Red channel | | 1 G Green channel | | 1 B Blue channel | | 1 W White channel (ignored) | | 2 transitionTime Transition time (x 100ms) | \*---------------------------------------------------------*/ uint8_t size = panel_ids.size(); uint8_t* message = (uint8_t*)malloc((size * 8) + 2); message[0] = (uint8_t)(size >> 8); /* nPanels H */ message[1] = (uint8_t)(size & 0xFF); /* nPanels L */ for(int i = 0; i < size; i++) { message[(8 * i) + 0 + 2] = (uint8_t)(panel_ids[i] >> 8); /* panelId H */ message[(8 * i) + 1 + 2] = (uint8_t)(panel_ids[i] & 0xFF); /* panelId L */ message[(8 * i) + 2 + 2] = (uint8_t)RGBGetRValue(colors[i]); /* R */ message[(8 * i) + 3 + 2] = (uint8_t)RGBGetGValue(colors[i]); /* G */ message[(8 * i) + 4 + 2] = (uint8_t)RGBGetBValue(colors[i]); /* B */ message[(8 * i) + 5 + 2] = (uint8_t)0; /* W */ message[(8 * i) + 6 + 2] = (uint8_t)0; /* transitionTime H */ message[(8 * i) + 7 + 2] = (uint8_t)0; /* transitionTime L */ } external_control_socket.udp_write((char *)message, (size * 8) + 2); free(message); } } void NanoleafController::StartExternalControl() { json request; request["write"]["command"] = "display"; request["write"]["animType"] = "extControl"; /*-------------------------------------------------------------*\ | Determine whether to use v1 or v2 extControl protocol based | | on model string | \*-------------------------------------------------------------*/ if(model == NANOLEAF_LIGHT_PANELS_MODEL) { /*---------------------------------------------------------*\ | Protocol v1 returns IP and port for UDP communication | \*---------------------------------------------------------*/ request["write"]["extControlVersion"] = "v1"; json response; if((APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request, &response) / 100) == 2) { external_control_socket.udp_client(response["streamControlIpAddr"].get().c_str(), std::to_string(response["streamControlPort"].get()).c_str()); selectedEffect = NANOLEAF_DIRECT_MODE_EFFECT_NAME; } } else if((model == NANOLEAF_CANVAS_MODEL) || (model == NANOLEAF_SHAPES_MODEL)) { /*---------------------------------------------------------*\ | Protocol v2 does not return anything, use device IP and | | port 60222 | \*---------------------------------------------------------*/ request["write"]["extControlVersion"] = "v2"; if((APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request) / 100) == 2) { external_control_socket.udp_client(address.c_str(), "60222"); selectedEffect = NANOLEAF_DIRECT_MODE_EFFECT_NAME; } } } void NanoleafController::SelectEffect(std::string effect_name) { json request; request["select"] = effect_name; if((APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request) / 100) == 2) { selectedEffect = effect_name; } } void NanoleafController::SetBrightness(int a_brightness) { json request; request["brightness"]["value"] = a_brightness; if((APIRequest("PUT", location, "/api/v1/"+auth_token+"/state", &request) / 100) == 2) { brightness = a_brightness; } } std::string NanoleafController::GetAuthToken() { return auth_token; }; std::string NanoleafController::GetName() { return name; }; std::string NanoleafController::GetSerial() { return serial; }; std::string NanoleafController::GetManufacturer() { return manufacturer; }; std::string NanoleafController::GetFirmwareVersion() { return firmware_version; }; std::string NanoleafController::GetModel() { return model; }; std::vector& NanoleafController::GetEffects() { return effects; }; std::vector& NanoleafController::GetPanelIds() { return panel_ids; }; std::string NanoleafController::GetSelectedEffect() { return selectedEffect; }; int NanoleafController::GetBrightness() { return brightness; };