From 14d543b9e4d3a40ee7cc4dad32e27c5d452d3732 Mon Sep 17 00:00:00 2001 From: Adam Honse Date: Thu, 23 Jul 2026 21:14:42 -0500 Subject: [PATCH] Initial commit for Linux-only controller for Valve Steam Machine using the leds-valve kernel module and sysfs interface --- .../RGBController_ValveSteamMachine_Linux.cpp | 172 +++++++++++++ .../RGBController_ValveSteamMachine_Linux.h | 34 +++ ...alveSteamMachineControllerDetect_Linux.cpp | 142 +++++++++++ .../ValveSteamMachineController_Linux.cpp | 227 ++++++++++++++++++ .../ValveSteamMachineController_Linux.h | 51 ++++ 5 files changed, 626 insertions(+) create mode 100644 Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.cpp create mode 100644 Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.h create mode 100644 Controllers/ValveSteamMachineController/ValveSteamMachineControllerDetect_Linux.cpp create mode 100644 Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.cpp create mode 100644 Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.h diff --git a/Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.cpp b/Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.cpp new file mode 100644 index 000000000..c4997ac19 --- /dev/null +++ b/Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.cpp @@ -0,0 +1,172 @@ +/*---------------------------------------------------------*\ +| RGBController_ValveSteamMachine_Linux.cpp | +| | +| RGBController for Valve Steam Machine LEDs | +| | +| Adam Honse (calcprogrammer1@gmail.com) 23 Jul 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "RGBController_ValveSteamMachine_Linux.h" +#include + +/**------------------------------------------------------------------*\ + @name Valve Steam Machine LEDs + @category LEDStrip + @type + @save :x: + @direct :white_check_mark: + @effects :white_check_mark: + @detectors DetectValveSteamMachineControllers + @comment +\*-------------------------------------------------------------------*/ + +RGBController_ValveSteamMachine::RGBController_ValveSteamMachine(ValveSteamMachineController* controller_ptr) +{ + controller = controller_ptr; + + name = controller->GetName(); + vendor = "Valve"; + type = DEVICE_TYPE_LEDSTRIP; + description = "Valve Steam Machine Device"; + location = controller->GetLEDPath(0); + + /*-----------------------------------------------------*\ + | If the device supports hardware effects, add them as | + | modes. Effects are stored as strings; the mode value | + | is the index into the available effects list. | + \*-----------------------------------------------------*/ + effects = controller->GetAvailableEffects(); + + for(unsigned int effect_idx = 0; effect_idx < effects.size(); effect_idx++) + { + unsigned int effect_color_mode; + unsigned int effect_flags; + std::string effect_name = effects[effect_idx]; + + /*-------------------------------------------------*\ + | Determine flags and color mode for effect | + \*-------------------------------------------------*/ + if(effect_name == "off") + { + effect_flags = 0; + effect_color_mode = MODE_COLORS_NONE; + } + else if(effect_name == "rainbow" || effect_name == "demo") + { + effect_flags = MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS; + effect_color_mode = MODE_COLORS_RANDOM; + } + else + { + effect_flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS; + effect_color_mode = MODE_COLORS_PER_LED; + } + + if(effect_name == "breath" || effect_name == "patrol" || effect_name == "rainbow" || effect_name == "demo") + { + effect_flags |= MODE_FLAG_HAS_SPEED; + } + + /*---------------------------------------------*\ + | The OpenRGB common name for manual mode is | + | Direct | + \*---------------------------------------------*/ + if(effect_name == "manual") + { + effect_name = "Direct"; + } + else + { + if(!effect_name.empty()) + { + effect_name[0] = std::toupper((unsigned char)effect_name[0]); + } + } + + mode new_mode; + new_mode.name = effect_name; + new_mode.value = effect_idx; + new_mode.flags = effect_flags; + new_mode.color_mode = effect_color_mode; + new_mode.speed_min = 20; + new_mode.speed_max = 0; + new_mode.speed = 10; + new_mode.brightness_min = 0; + new_mode.brightness_max = 0xFF; + new_mode.brightness = 0x38; + modes.push_back(new_mode); + } + + SetupZones(); +} + +RGBController_ValveSteamMachine::~RGBController_ValveSteamMachine() +{ + Shutdown(); + + delete controller; +} + +void RGBController_ValveSteamMachine::SetupZones() +{ + /*-----------------------------------------------------*\ + | Create a single linear zone containing all LEDs | + \*-----------------------------------------------------*/ + zone led_zone; + led_zone.name = "Steam Machine LEDs"; + led_zone.type = ZONE_TYPE_LINEAR; + led_zone.leds_min = (unsigned int)controller->GetLEDCount(); + led_zone.leds_max = (unsigned int)controller->GetLEDCount(); + led_zone.leds_count = (unsigned int)controller->GetLEDCount(); + + zones.push_back(led_zone); + + /*-----------------------------------------------------*\ + | Create LED entries for each LED in the controller | + \*-----------------------------------------------------*/ + for(unsigned int led_idx = 0; led_idx < controller->GetLEDCount(); led_idx++) + { + led new_led; + new_led.name = "Steam Machine LED " + std::to_string(led_idx); + new_led.value = led_idx; + + leds.push_back(new_led); + } + + SetupColors(); +} + +void RGBController_ValveSteamMachine::DeviceUpdateLEDs() +{ + for(unsigned int led_idx = 0; led_idx < controller->GetLEDCount(); led_idx++) + { + DeviceUpdateSingleLED(led_idx); + } +} + +void RGBController_ValveSteamMachine::DeviceUpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_ValveSteamMachine::DeviceUpdateSingleLED(int led) +{ + unsigned char red = RGBGetRValue(colors[led]); + unsigned char grn = RGBGetGValue(colors[led]); + unsigned char blu = RGBGetBValue(colors[led]); + + controller->SetLEDColor(led, red, grn, blu); +} + +void RGBController_ValveSteamMachine::DeviceUpdateMode() +{ + if((unsigned int)active_mode < effects.size()) + { + controller->SetEffect(effects[active_mode]); + controller->SetBrightness(modes[active_mode].brightness); + controller->SetDelay(modes[active_mode].speed); + } +} diff --git a/Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.h b/Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.h new file mode 100644 index 000000000..29a55f13d --- /dev/null +++ b/Controllers/ValveSteamMachineController/RGBController_ValveSteamMachine_Linux.h @@ -0,0 +1,34 @@ +/*---------------------------------------------------------*\ +| RGBController_ValveSteamMachine_Linux.h | +| | +| RGBController for Valve Steam Machine LEDs | +| | +| Adam Honse (calcprogrammer1@gmail.com) 23 Jul 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "ValveSteamMachineController_Linux.h" + +class RGBController_ValveSteamMachine : public RGBController +{ +public: + RGBController_ValveSteamMachine(ValveSteamMachineController* controller_ptr); + ~RGBController_ValveSteamMachine(); + + void SetupZones(); + + void DeviceUpdateLEDs(); + void DeviceUpdateZoneLEDs(int zone); + void DeviceUpdateSingleLED(int led); + + void DeviceUpdateMode(); + +private: + ValveSteamMachineController* controller; + std::vector effects; +}; \ No newline at end of file diff --git a/Controllers/ValveSteamMachineController/ValveSteamMachineControllerDetect_Linux.cpp b/Controllers/ValveSteamMachineController/ValveSteamMachineControllerDetect_Linux.cpp new file mode 100644 index 000000000..6d0a62515 --- /dev/null +++ b/Controllers/ValveSteamMachineController/ValveSteamMachineControllerDetect_Linux.cpp @@ -0,0 +1,142 @@ +/*---------------------------------------------------------*\ +| ValveSteamMachineControllerDetect_Linux.cpp | +| | +| Detector for Valve Steam Machine LEDs | +| | +| Adam Honse (calcprogrammer1@gmail.com) 23 Jul 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include +#include +#include +#include "DetectionManager.h" +#include "filesystem.h" +#include "RGBController_ValveSteamMachine_Linux.h" + +/*---------------------------------------------------------*\ +| Helper function to extract the numeric index enclosed | +| in brackets from a LED directory name for proper | +| numerical sorting. | +| For example, "valve-leds[2]" -> 2, "valve-leds[10]" -> 10.| +\*---------------------------------------------------------*/ +static int GetLEDNumber(const std::string& path) +{ + /*-----------------------------------------------------*\ + | Extract the directory name from the full path | + \*-----------------------------------------------------*/ + std::string dir_name = filesystem::path(path).filename().string(); + + /*-----------------------------------------------------*\ + | Find the last '[' and ']' in the directory name to | + | extract the numeric index enclosed in brackets. | + | For example, "valve-leds[10]" -> 10. | + \*-----------------------------------------------------*/ + size_t bracket_start = dir_name.rfind('['); + size_t bracket_end = dir_name.rfind(']'); + + if(bracket_start != std::string::npos && + bracket_end != std::string::npos && + bracket_end > bracket_start) + { + std::string num_str = dir_name.substr(bracket_start + 1, bracket_end - bracket_start - 1); + + /*-------------------------------------------------*\ + | Verify the extracted substring is all digits | + | before converting, to avoid exceptions from | + | std::stoi. | + \*-------------------------------------------------*/ + bool all_digits = true; + + for(char c : num_str) + { + if(!std::isdigit(static_cast(c))) + { + all_digits = false; + break; + } + } + + if(all_digits && !num_str.empty()) + { + return(std::stoi(num_str)); + } + } + + return(0); +} + +/*---------------------------------------------------------*\ +| Sort from highest to lowest, as Steam Machine LEDs go | +| from 0 on the right to 16 on the left | +\*---------------------------------------------------------*/ +static bool CompareLEDPaths(const std::string& a, const std::string& b) +{ + return(GetLEDNumber(a) > GetLEDNumber(b)); +} + +/*---------------------------------------------------------*\ +| Detect leds-valve sysfs LEDs | +\*---------------------------------------------------------*/ +void DetectValveLEDs(std::vector& led_paths) +{ + const std::string leds_class_path = "/sys/class/leds"; + + if(!filesystem::exists(leds_class_path)) + { + return; + } + + for(const filesystem::directory_entry& entry : filesystem::directory_iterator(leds_class_path)) + { + std::string led_dir = entry.path().string(); + + if(led_dir.find("valve-leds") != std::string::npos) + { + led_paths.push_back(led_dir); + } + } + + /*-----------------------------------------------------*\ + | Sort the LED paths numerically by their bracket | + | enclosed index to ensure correct ordering from [N] | + | to [0] | + \*-----------------------------------------------------*/ + std::sort(led_paths.begin(), led_paths.end(), CompareLEDPaths); +} + +DetectedControllers DetectValveSteamMachineControllers() +{ + DetectedControllers detected_controllers; + + /*-----------------------------------------------------*\ + | Detect Steam Machine (leds-valve) LED controller | + \*-----------------------------------------------------*/ + if(detected_controllers.size() == 0) + { + std::vector valve_led_paths; + + DetectValveLEDs(valve_led_paths); + + if(valve_led_paths.size() > 0) + { + ValveSteamMachineController* controller = new ValveSteamMachineController("Valve Steam Machine"); + + for(unsigned int led_idx = 0; led_idx < valve_led_paths.size(); led_idx++) + { + controller->AddLED(valve_led_paths[led_idx]); + } + + RGBController_ValveSteamMachine* rgb_controller = new RGBController_ValveSteamMachine(controller); + + detected_controllers.push_back(rgb_controller); + } + } + + return(detected_controllers); +} + +REGISTER_DETECTOR("Valve Steam Machine", DetectValveSteamMachineControllers); \ No newline at end of file diff --git a/Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.cpp b/Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.cpp new file mode 100644 index 000000000..c35422e6b --- /dev/null +++ b/Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.cpp @@ -0,0 +1,227 @@ +/*---------------------------------------------------------*\ +| ValveSteamMachineController_Linux.cpp | +| | +| Driver for Valve Steam Machine LEDs | +| | +| Adam Honse (calcprogrammer1@gmail.com) 23 Jul 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "ValveSteamMachineController_Linux.h" +#include +#include +#include + +ValveSteamMachineController::ValveSteamMachineController(std::string dev_name) +{ + name = dev_name; +} + +ValveSteamMachineController::~ValveSteamMachineController() +{ + +} + +std::string ValveSteamMachineController::GetName() +{ + return(name); +} + +void ValveSteamMachineController::AddLED(std::string led_path) +{ + /*-----------------------------------------------------*\ + | Ensure the path ends with a slash | + \*-----------------------------------------------------*/ + std::string path = led_path; + + if(path.back() != '/') + { + path += '/'; + } + + led_paths.push_back(path); + + /*-----------------------------------------------------*\ + | Open the brightness and multi_intensity files | + \*-----------------------------------------------------*/ + std::ofstream brightness_file; + brightness_file.open(path + "brightness"); + led_brightness.push_back(std::move(brightness_file)); + + std::ofstream multi_intensity_file; + multi_intensity_file.open(path + "multi_intensity"); + led_multi_intensity.push_back(std::move(multi_intensity_file)); + + /*-----------------------------------------------------*\ + | Open the brightness_scale and delay files on the | + | first LED only. Setting these on any one LED applies | + | to the entire strip, so we only need to open it once. | + \*-----------------------------------------------------*/ + if(led_paths.size() == 1) + { + led_brightness_scale.open(path + "brightness_scale"); + led_delay.open(path + "delay"); + } + + /*-----------------------------------------------------*\ + | Check if the effect_index file exists to determine | + | if hardware effects are supported by this LED. The | + | effect_index file contains a list of available effect | + | names, while the effect file is used to set the | + | active effect. | + \*-----------------------------------------------------*/ + std::ifstream effect_index_test(path + "effect_index"); + + if(effect_index_test.good()) + { + effect_index_test.close(); + + std::ofstream effect_file; + effect_file.open(path + "effect"); + led_effect.push_back(std::move(effect_file)); + + /*-------------------------------------------------*\ + | Read available effects from the first LED only | + \*-------------------------------------------------*/ + if(led_paths.size() == 1) + { + ReadAvailableEffects(path); + } + } + else + { + effect_index_test.close(); + } +} + +size_t ValveSteamMachineController::GetLEDCount() +{ + return(led_paths.size()); +} + +std::string ValveSteamMachineController::GetLEDPath(unsigned int led_idx) +{ + if(led_idx < led_paths.size()) + { + return(led_paths[led_idx]); + } + + return(""); +} + +std::vector ValveSteamMachineController::GetAvailableEffects() +{ + return(available_effects); +} + +void ValveSteamMachineController::ReadAvailableEffects(std::string first_led_path) +{ + std::ifstream effect_index_file(first_led_path + "effect_index"); + + if(!effect_index_file.good()) + { + return; + } + + /*-----------------------------------------------------*\ + | The effect_index file contains a list of available | + | effect names separated by whitespace. The effect | + | file itself gives the currently active effect when | + | read, and is used to set the active effect when | + | written. | + \*-----------------------------------------------------*/ + std::string content; + std::string line; + + while(std::getline(effect_index_file, line)) + { + content += line + " "; + } + + effect_index_file.close(); + + std::istringstream stream(content); + std::string token; + + while(stream >> token) + { + /*-------------------------------------------------*\ + | Add to list if not already present | + \*-------------------------------------------------*/ + if(std::find(available_effects.begin(), available_effects.end(), token) == available_effects.end()) + { + available_effects.push_back(token); + } + } +} + +void ValveSteamMachineController::SetLEDColor(unsigned int led_idx, unsigned char red, unsigned char grn, unsigned char blu) +{ + if(led_idx >= led_paths.size()) + { + return; + } + + /*-----------------------------------------------------*\ + | Use multi_intensity to set RGB values. This is the | + | leds-valve sysfs LED interface pattern. | + \*-----------------------------------------------------*/ + std::string color_str = std::to_string((unsigned int)red) + " " + + std::to_string((unsigned int)grn) + " " + + std::to_string((unsigned int)blu); + + led_multi_intensity[led_idx].write(color_str.c_str(), color_str.length()); + led_multi_intensity[led_idx].flush(); +} + +void ValveSteamMachineController::SetLEDEffect(unsigned int led_idx, std::string effect) +{ + if(led_idx >= led_paths.size()) + { + return; + } + + if(led_effect.size() <= led_idx) + { + return; + } + + led_effect[led_idx].write(effect.c_str(), effect.length()); + led_effect[led_idx].flush(); +} + +void ValveSteamMachineController::SetEffect(std::string effect) +{ + /*-----------------------------------------------------*\ + | Setting the effect on one LED applies it to all LEDs | + | on the device, so we only need to set it on the first | + | LED | + \*-----------------------------------------------------*/ + if(led_effect.size() > 0) + { + led_effect[0].write(effect.c_str(), effect.length()); + led_effect[0].flush(); + } +} + +void ValveSteamMachineController::SetBrightness(unsigned int brightness) +{ + if(led_brightness_scale.is_open()) + { + std::string brightness_str = std::to_string(brightness); + led_brightness_scale.write(brightness_str.c_str(), brightness_str.length()); + led_brightness_scale.flush(); + } +} + +void ValveSteamMachineController::SetDelay(unsigned int delay) +{ + if(led_delay.is_open()) + { + std::string delay_str = std::to_string(delay); + led_delay.write(delay_str.c_str(), delay_str.length()); + led_delay.flush(); + } +} \ No newline at end of file diff --git a/Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.h b/Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.h new file mode 100644 index 000000000..0021e729c --- /dev/null +++ b/Controllers/ValveSteamMachineController/ValveSteamMachineController_Linux.h @@ -0,0 +1,51 @@ +/*---------------------------------------------------------*\ +| ValveSteamMachineController_Linux.h | +| | +| Driver for Valve Steam Machine LEDs | +| | +| Adam Honse (calcprogrammer1@gmail.com) 23 Jul 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +class ValveSteamMachineController +{ +public: + ValveSteamMachineController(std::string dev_name); + ~ValveSteamMachineController(); + + std::string GetName(); + + void AddLED(std::string led_path); + size_t GetLEDCount(); + + std::string GetLEDPath(unsigned int led_idx); + + std::vector GetAvailableEffects(); + bool SupportsEffects(); + + void SetLEDColor(unsigned int led_idx, unsigned char red, unsigned char grn, unsigned char blu); + void SetLEDEffect(unsigned int led_idx, std::string effect); + void SetEffect(std::string effect); + void SetBrightness(unsigned int brightness); + void SetDelay(unsigned int delay); + +private: + std::string name; + std::vector led_paths; + std::vector led_brightness; + std::vector led_multi_intensity; + std::vector led_effect; + std::ofstream led_brightness_scale; + std::ofstream led_delay; + std::vector available_effects; + + void ReadAvailableEffects(std::string first_led_path); +};