/*-----------------------------------------*\ | AlienwareController.cpp | | | | Driver for Alienware lighting controller | | | | Gabriel Marcano (gemarcano) 4/21/2021 | \*-----------------------------------------*/ #include "RGBController.h" #include "AlienwareController.h" #include #include #include #include #include #include #include typedef uint32_t alienware_platform_id; /*---------------------------------------------------------*\ | Some devices appear to report the wrong number of zones. | | Record that here. | \*---------------------------------------------------------*/ static const std::map zone_quirks_table = { { 0x0C01, 4 } // Dell G5 SE 5505 }; /*---------------------------------------------------------*\ | Add zones for devices here, mapping the platform ID to | | the zone names | \*---------------------------------------------------------*/ static const std::map> zone_names_table = { { 0x0C01, { "Left", "Middle", "Right", "Numpad" } } }; static void SendHIDReport(hid_device *dev, const unsigned char* usb_buf, size_t usb_buf_size) { using namespace std::chrono_literals; hid_send_feature_report(dev, usb_buf, usb_buf_size); /*-----------------------------------------------------*\ | The controller really doesn't like really spammed by | | too many commands at once... the delay may be command | | dependent also. Delay for longer if the command is | | changing animation state | \*-----------------------------------------------------*/ unsigned char command = usb_buf[2]; unsigned char subcommand = usb_buf[3]; if( ( command == ALIENWARE_COMMAND_USER_ANIM ) && ( ( subcommand == ALIENWARE_COMMAND_USER_ANIM_FINISH_PLAY ) || ( subcommand == ALIENWARE_COMMAND_USER_ANIM_FINISH_SAVE ) ) ) { std::this_thread::sleep_for(1s); } else { std::this_thread::sleep_for(60ms); } } AlienwareController::AlienwareController(hid_device* dev_handle, const hid_device_info& info, std::string name) { HidapiAlienwareReport report; dev = dev_handle; device_name = name; location = info.path; /*-----------------------------------------------------*\ | Get serial number | \*-----------------------------------------------------*/ std::wstring tmp_serial_number; tmp_serial_number = info.serial_number; serial_number = std::string(tmp_serial_number.begin(), tmp_serial_number.end()); /*-----------------------------------------------------*\ | Get zone information by checking firmware | | configuration | \*-----------------------------------------------------*/ report = Report(ALIENWARE_COMMAND_REPORT_CONFIG); alienware_platform_id platform_id = report.data[4] << 8 | report.data[5]; /*-----------------------------------------------------*\ | Get firmware version | \*-----------------------------------------------------*/ report = Report(ALIENWARE_COMMAND_REPORT_FIRMWARE); std::stringstream fw_string; fw_string << static_cast(report.data[4]) << '.' << static_cast(report.data[5]) << '.' << static_cast(report.data[6]); version = fw_string.str(); /*-----------------------------------------------------*\ | Check if the device reports the wrong number of zones | \*-----------------------------------------------------*/ unsigned number_of_zones = zone_quirks_table.count(platform_id) ? zone_quirks_table.at(platform_id) : report.data[6]; /*-----------------------------------------------------*\ | Initialize Alienware zones | \*-----------------------------------------------------*/ zones.resize(number_of_zones); if(zone_names_table.count(platform_id)) { zone_names = zone_names_table.at(platform_id); } else { /*-------------------------------------------------*\ | If this is an unknown controller, set the name of | | all regions to "Unknown" | \*-------------------------------------------------*/ for(size_t i = 0; i < number_of_zones; i++) { zone_names.emplace_back("Unknown"); } } /*-----------------------------------------------------*\ | Set defaults for all zones | | It doesn't seem possible to read the controller's | | current state, hence the default value being set here.| \*-----------------------------------------------------*/ for(unsigned int zone_idx; zone_idx < zones.size(); zone_idx++) { zones[zone_idx].color[0] = 0x000000; zones[zone_idx].color[1] = 0x000000; zones[zone_idx].mode = ALIENWARE_MODE_COLOR; /*-------------------------------------------------*\ | Default period value from ACC | \*-------------------------------------------------*/ zones[zone_idx].period = 2000; zones[zone_idx].tempo = ALIENWARE_TEMPO_MAX; zones[zone_idx].dim = 0; } /*-----------------------------------------------------*\ | Initialize dirty flags | \*-----------------------------------------------------*/ dirty = true; dirty_dim = true; } AlienwareController::~AlienwareController() { } unsigned int AlienwareController::GetZoneCount() { return(zones.size()); } std::vector AlienwareController::GetZoneNames() { return(zone_names); } std::string AlienwareController::GetDeviceLocation() { return("HID: " + location); } std::string AlienwareController::GetDeviceName() { return(device_name); } std::string AlienwareController::GetSerialString() { return(serial_number); } std::string AlienwareController::GetFirmwareVersion() { return(version); } AlienwareController::HidapiAlienwareReport AlienwareController::GetResponse() { /*-----------------------------------------------------*\ | Zero init. This is not updated if there's a problem. | \*-----------------------------------------------------*/ HidapiAlienwareReport result; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(result.data, 0x00, sizeof(result.data)); hid_get_feature_report(dev, result.data, HIDAPI_ALIENWARE_REPORT_SIZE); return(result); } AlienwareController::HidapiAlienwareReport AlienwareController::Report(uint8_t subcommand) { unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Set up message packet with leading 00, per hidapi | \*-----------------------------------------------------*/ usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_REPORT; usb_buf[0x03] = subcommand; /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); return(GetResponse()); } AlienwareReport AlienwareController::GetStatus(uint8_t subcommand) { HidapiAlienwareReport data = Report(subcommand); AlienwareReport result = AlienwareReport{}; /*-----------------------------------------------------*\ | Skip first byte, as that's the report number, which | | should be 0 | \*-----------------------------------------------------*/ memcpy(result.data, &data.data[1], sizeof(result.data)); return(result); } bool AlienwareController::Dim(std::vector zones, double percent) { /*-----------------------------------------------------*\ | Bail out if there are no zones to update | \*-----------------------------------------------------*/ if(!zones.size()) { return(true); } unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Set up message packet with leading 00, per hidapi | \*-----------------------------------------------------*/ uint16_t num_zones = zones.size(); usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_DIM; usb_buf[0x03] = static_cast(percent * 0x64); usb_buf[0x04] = num_zones >> 8; usb_buf[0x05] = num_zones & 0xFF; for(size_t i = 0; i < num_zones; i++) { usb_buf[0x06+i] = zones[i]; } /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); HidapiAlienwareReport response; response = GetResponse(); /*-----------------------------------------------------*\ | For this command, error is if the output equals the | | input | \*-----------------------------------------------------*/ return((response.data[1] == 0x03) && memcmp(usb_buf, response.data, HIDAPI_ALIENWARE_REPORT_SIZE)); } bool AlienwareController::UserAnimation(uint16_t subcommand, uint16_t animation, uint16_t duration) { unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Set up message packet with leading 00 per hidapi | \*-----------------------------------------------------*/ usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_USER_ANIM; usb_buf[0x03] = subcommand >> 8; usb_buf[0x04] = subcommand & 0xFF; usb_buf[0x05] = animation >> 8; usb_buf[0x06] = animation & 0xFF; usb_buf[0x07] = duration >> 8; usb_buf[0x08] = duration & 0xFF; /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Every subcommand appears to report its result on a | | different byte | \*-----------------------------------------------------*/ HidapiAlienwareReport response; response = GetResponse(); /*-----------------------------------------------------*\ | The only time the 0x03 byte is zero is if the | | controller has crashed | \*-----------------------------------------------------*/ if(response.data[1] == 0) { return(false); } switch(subcommand) { case ALIENWARE_COMMAND_USER_ANIM_FINISH_SAVE: return(!response.data[7]); case ALIENWARE_COMMAND_USER_ANIM_FINISH_PLAY: return(!response.data[5]); case ALIENWARE_COMMAND_USER_ANIM_PLAY: return(!response.data[7]); default: return(true); } } bool AlienwareController::SelectZones(const std::vector& zones) { /*-----------------------------------------------------*\ | Bail if zones is empty, and return false to indicate | | nothing has changed | \*-----------------------------------------------------*/ if(!zones.size()) { return(false); } unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Set up message packet with leading 00, per hidapi | \*-----------------------------------------------------*/ uint16_t num_zones = zones.size(); usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_SELECT_ZONES; usb_buf[0x03] = 1; // loop? usb_buf[0x04] = num_zones >> 8; usb_buf[0x05] = num_zones & 0xFF; for(size_t i = 0; i < num_zones; i++) { usb_buf[0x06+i] = zones[i]; } /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); HidapiAlienwareReport response; response = GetResponse(); /*-----------------------------------------------------*\ | For this command, error is if the output equals the | | input | \*-----------------------------------------------------*/ return((response.data[1] == 0x03) && memcmp(usb_buf, response.data, HIDAPI_ALIENWARE_REPORT_SIZE)); } bool AlienwareController::ModeAction(uint8_t mode, uint16_t duration, uint16_t tempo, RGBColor color) { return(ModeAction(&mode, &duration, &tempo, &color, 1)); } bool AlienwareController::ModeAction ( const uint8_t* mode, const uint16_t* duration, const uint16_t* tempo, const RGBColor* color, unsigned amount ) { unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Amount must be 3 or less, as that's how many | | subcommands can fit into one report | \*-----------------------------------------------------*/ if(amount > 3) { return(false); } /*-----------------------------------------------------*\ | Set up message packet with leading 00, per hidapi | \*-----------------------------------------------------*/ usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_ADD_ACTION; for(unsigned int i = 0; i < amount; i++) { usb_buf[0x03 + (8 * i)] = mode[i]; usb_buf[0x04 + (8 * i)] = duration[i] >> 8; usb_buf[0x05 + (8 * i)] = duration[i] & 0xFF; usb_buf[0x06 + (8 * i)] = tempo[i] >> 8; usb_buf[0x07 + (8 * i)] = tempo[i] & 0xFF; usb_buf[0x08 + (8 * i)] = RGBGetRValue(color[i]); usb_buf[0x09 + (8 * i)] = RGBGetGValue(color[i]); usb_buf[0x0A + (8 * i)] = RGBGetBValue(color[i]); } /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); HidapiAlienwareReport response; response = GetResponse(); /*-----------------------------------------------------*\ | For this command, error is if the output equals the | | input | \*-----------------------------------------------------*/ return((response.data[1] == 0x03) && memcmp(usb_buf, response.data, HIDAPI_ALIENWARE_REPORT_SIZE)); } bool AlienwareController::MultiModeAction ( const uint8_t* mode, const uint16_t* duration, const uint16_t* tempo, const RGBColor* color, unsigned amount ) { bool result = true; unsigned int left = amount; while(left && result) { unsigned int tmp_amount; tmp_amount = std::min(left, 3u); result &= ModeAction(mode, duration, tempo, color, tmp_amount); mode += tmp_amount; duration += tmp_amount; tempo += tmp_amount; color += tmp_amount; left -= tmp_amount; } return(result); } bool AlienwareController::SetColorDirect(RGBColor color, std::vector zones) { /*-----------------------------------------------------*\ | Bail if zones is empty | \*-----------------------------------------------------*/ if(zones.empty()) { return(true); } unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Set up message packet with leading 00, per hidapi | \*-----------------------------------------------------*/ uint16_t num_zones = zones.size(); usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_SET_COLOR; usb_buf[0x03] = RGBGetRValue(color); usb_buf[0x04] = RGBGetGValue(color); usb_buf[0x05] = RGBGetBValue(color); usb_buf[0x06] = num_zones >> 8; usb_buf[0x07] = num_zones & 0xFF; for(size_t i = 0; i < num_zones; i++) { usb_buf[0x08 + i] = zones[i]; } /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); HidapiAlienwareReport response; response = GetResponse(); /*-----------------------------------------------------*\ | For this command, error is if the output equals the | | input | \*-----------------------------------------------------*/ return((response.data[1] == 0x03) && memcmp(usb_buf, response.data, HIDAPI_ALIENWARE_REPORT_SIZE)); } bool AlienwareController::Reset() { /*-----------------------------------------------------*\ | Bail if zones is empty | \*-----------------------------------------------------*/ if(zones.empty()) { return(true); } unsigned char usb_buf[HIDAPI_ALIENWARE_REPORT_SIZE]; /*-----------------------------------------------------*\ | Zero out buffer | \*-----------------------------------------------------*/ memset(usb_buf, 0x00, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Set up message packet with leading 00, per hidapi | \*-----------------------------------------------------*/ usb_buf[0x00] = 0x00; usb_buf[0x01] = 0x03; usb_buf[0x02] = ALIENWARE_COMMAND_RESET; /*-----------------------------------------------------*\ | Send packet | \*-----------------------------------------------------*/ SendHIDReport(dev, usb_buf, sizeof(usb_buf)); HidapiAlienwareReport response; response = GetResponse(); /*-----------------------------------------------------*\ | For this command, error is if the output equals the | | input | \*-----------------------------------------------------*/ return(response.data[1] == 0x03); } void AlienwareController::SetMode(uint8_t zone, uint8_t mode) { if(mode != zones[zone].mode) { zones[zone].mode = mode; dirty = true; } } void AlienwareController::SetColor(uint8_t zone, RGBColor color) { SetColor(zone, color, zones[zone].color[1]); } void AlienwareController::SetColor(uint8_t zone, RGBColor color1, RGBColor color2) { if((color1 != zones[zone].color[0])) { zones[zone].color[0] = color1; dirty = true; } if((color2 != zones[zone].color[1])) { zones[zone].color[1] = color2; dirty = true; } } void AlienwareController::SetPeriod(uint8_t zone, uint16_t period) { if(period != zones[zone].period) { zones[zone].period = period; dirty = true; } } void AlienwareController::SetTempo(uint8_t zone, uint16_t tempo) { if(tempo != zones[zone].tempo) { zones[zone].tempo = tempo; dirty = true; } } void AlienwareController::SetDim(uint8_t zone, uint8_t dim) { /*-----------------------------------------------------*\ | Clamp dim to values between 0 and 100 | \*-----------------------------------------------------*/ if(dim > 100) { dim = 100; } if(dim != zones[zone].dim) { zones[zone].dim = dim; dirty_dim = true; } } void AlienwareController::UpdateDim() { if(!dirty_dim) { return; } /*-----------------------------------------------------*\ | Collect all zones that share dim settings, and update | | them together | \*-----------------------------------------------------*/ std::map> dim_zone_map; for(size_t i = 0; i < zones.size(); i++) { dim_zone_map[zones[i].dim].emplace_back(i); } for(std::pair> &pair : dim_zone_map) { /*-------------------------------------------------*\ | Bail on an error... | \*-------------------------------------------------*/ if(!Dim(pair.second, pair.first)) { return; } } dirty_dim = false; } bool AlienwareController::UpdateDirect() { /*-----------------------------------------------------*\ | Collect all zones that share dim settings, and update | | them together | \*-----------------------------------------------------*/ std::map> color_zone_map; for(size_t i = 0; i < zones.size(); i++) { color_zone_map[zones[i].color[0]].emplace_back(i); } for(std::pair> &pair : color_zone_map) { /*-------------------------------------------------*\ | Bail on an error... | \*-------------------------------------------------*/ if(!SetColorDirect(pair.first, pair.second)) { return false; } } return true; } static const RGBColor rainbow_colors[4][7] = { { 0xFF0000, 0xFFA500, 0xFFFF00, 0x008000, 0x00BFFF, 0x0000FF, 0x800080 }, { 0x800080, 0xFF0000, 0xFFA500, 0xFFFF00, 0x008000, 0x00BFFF, 0x0000FF }, { 0x0000FF, 0x800080, 0xFF0000, 0xFFA500, 0xFFFF00, 0x008000, 0x00BFFF }, { 0x00BFFF, 0x0000FF, 0x800080, 0xFF0000, 0xFFA500, 0xFFFF00, 0x008000 } }; void AlienwareController::UpdateMode() { /*-----------------------------------------------------*\ | If there are no updates, don't bother running this | \*-----------------------------------------------------*/ if(!dirty) { return; } bool result = UserAnimation(ALIENWARE_COMMAND_USER_ANIM_NEW, ALIENWARE_COMMAND_USER_ANIM_KEYBOARD, 0); if(!result) { return; } for(std::size_t zone_idx = 0; zone_idx < zones.size(); zone_idx++) { alienware_zone zone = zones[zone_idx]; result = SelectZones({static_cast(zone_idx)}); if(!result) { return; } /*-------------------------------------------------*\ | Some modes use 0x07D0 for their duration as sent | | by AWCC traces, maybe 2000ms? | \*-------------------------------------------------*/ switch (zone.mode) { case ALIENWARE_MODE_COLOR: result = ModeAction(zone.mode, 2000, ALIENWARE_TEMPO_MAX, zone.color[0]); break; case ALIENWARE_MODE_PULSE: result = ModeAction(zone.mode, zone.period, zone.tempo, zone.color[0]); break; case ALIENWARE_MODE_MORPH: { uint8_t zones[2] = { zone.mode, zone.mode }; uint16_t periods[2] = { zone.period, zone.period }; uint16_t tempos[2] = { zone.tempo, zone.tempo }; RGBColor colors[2] = { zone.color[0], zone.color[1] }; result = MultiModeAction(zones, periods, tempos, colors, 2); } break; case ALIENWARE_MODE_SPECTRUM: { uint8_t zones[7] = { ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH }; uint16_t periods[7] = { zone.period, zone.period, zone.period, zone.period, zone.period, zone.period, zone.period }; uint16_t tempos[7] = { zone.tempo, zone.tempo, zone.tempo, zone.tempo, zone.tempo, zone.tempo, zone.tempo }; result = MultiModeAction(zones, periods, tempos, rainbow_colors[0], 7); } break; case ALIENWARE_MODE_RAINBOW: { uint8_t zones[7] = { ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH }; uint16_t periods[7] = { zone.period, zone.period, zone.period, zone.period, zone.period, zone.period, zone.period }; uint16_t tempos[7] = { zone.tempo, zone.tempo, zone.tempo, zone.tempo, zone.tempo, zone.tempo, zone.tempo }; result = MultiModeAction(zones, periods, tempos, rainbow_colors[zone_idx], 7); } break; case ALIENWARE_MODE_BREATHING: { uint8_t zones[2] = { ALIENWARE_MODE_MORPH, ALIENWARE_MODE_MORPH }; uint16_t periods[2] = { zone.period, zone.period }; uint16_t tempos[2] = { zone.tempo, zone.tempo }; RGBColor colors[2] = { zone.color[0], 0x0 }; result = MultiModeAction(zones, periods, tempos, colors, 2); } break; default: result = false; } if(!result) { return; } } result = UserAnimation(ALIENWARE_COMMAND_USER_ANIM_FINISH_PLAY, ALIENWARE_COMMAND_USER_ANIM_KEYBOARD, 0); /*-------------------------------------------------*\ | Don't update dirty flag if there's an error | \*-------------------------------------------------*/ if(!result) { return; } dirty = false; } void AlienwareController::UpdateController() { UpdateMode(); UpdateDim(); }