/*-----------------------------------------*\ | NanoleafController.cpp | | | | API Interface for Nanoleaf devices | | | | Nikita Rushmanov 01/13/2022 | \*-----------------------------------------*/ #include "NanoleafController.h" #include "LogManager.h" #include "httplib.h" long APIRequest(std::string method, std::string location, std::string URI, json* request_data = nullptr, json* response_data = nullptr) { /*-------------------------------------------------------------*\ | Append http:// to the location field to create the URL | \*-------------------------------------------------------------*/ const std::string url("http://" + location); /*-------------------------------------------------------------*\ | Create httplib Client and variables to hold result | \*-------------------------------------------------------------*/ httplib::Client client(url.c_str()); int status = 0; std::string body = ""; /*-------------------------------------------------------------*\ | Perform the appropriate call for the given method | \*-------------------------------------------------------------*/ if(method == "GET") { httplib::Result result = client.Get(URI.c_str()); status = result->status; body = result->body; } else if(method == "PUT") { if(request_data) { httplib::Result result = client.Put(URI.c_str(), request_data->dump(), "application/json"); status = result->status; body = result->body; } else { httplib::Result result = client.Put(URI.c_str()); status = result->status; body = result->body; } } else if(method == "DELETE") { httplib::Result result = client.Delete(URI.c_str()); status = result->status; body = result->body; } else if(method == "POST") { httplib::Result result = client.Post(URI.c_str()); status = result->status; body = result->body; } /*-------------------------------------------------------------*\ | If status is in the 200 range the request was successful | \*-------------------------------------------------------------*/ if((status / 100) == 2) { if(response_data) { *response_data = json::parse(body); } } else { LOG_DEBUG("[Nanoleaf] HTTP %i:Could not %s from %s", status, method.c_str(), url.c_str()); } return status; } 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; };