From cb93f29506ee98233bd0042a8196beb6fb455e0c Mon Sep 17 00:00:00 2001 From: Gabriel Popa Date: Wed, 10 Jun 2026 12:22:35 +0200 Subject: [PATCH 1/3] Add NuPhy Air V2 keyboard support --- .../NuPhyKeyboardController.cpp | 167 ++++++++++++++ .../NuPhyKeyboardController.h | 47 ++++ .../NuPhyKeyboardControllerDetect.cpp | 44 ++++ .../RGBController_NuPhyKeyboard.cpp | 203 ++++++++++++++++++ .../RGBController_NuPhyKeyboard.h | 31 +++ 5 files changed, 492 insertions(+) create mode 100644 Controllers/NuPhyKeyboardController/NuPhyKeyboardController.cpp create mode 100644 Controllers/NuPhyKeyboardController/NuPhyKeyboardController.h create mode 100644 Controllers/NuPhyKeyboardController/NuPhyKeyboardControllerDetect.cpp create mode 100644 Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp create mode 100644 Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.h diff --git a/Controllers/NuPhyKeyboardController/NuPhyKeyboardController.cpp b/Controllers/NuPhyKeyboardController/NuPhyKeyboardController.cpp new file mode 100644 index 000000000..078f48d64 --- /dev/null +++ b/Controllers/NuPhyKeyboardController/NuPhyKeyboardController.cpp @@ -0,0 +1,167 @@ +/*---------------------------------------------------------*\ +| NuPhyKeyboardController.cpp | +| | +| Driver for NuPhy QMK/VIA RGB Matrix keyboards | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include "NuPhyKeyboardController.h" +#include "StringUtils.h" + +#define NUPHY_VIA_MESSAGE_LENGTH 32 +#define NUPHY_VIA_GET_PROTOCOL_VERSION 0x01 +#define NUPHY_VIA_CUSTOM_SET_VALUE 0x07 +#define NUPHY_VIA_CUSTOM_GET_VALUE 0x08 +#define NUPHY_VIA_CUSTOM_SAVE 0x09 +#define NUPHY_VIA_RGB_MATRIX_CHANNEL 0x03 +#define NUPHY_VIA_RGB_MATRIX_BRIGHTNESS 0x01 +#define NUPHY_VIA_RGB_MATRIX_EFFECT 0x02 +#define NUPHY_VIA_RGB_MATRIX_SPEED 0x03 +#define NUPHY_VIA_RGB_MATRIX_COLOR 0x04 + +NuPhyKeyboardController::NuPhyKeyboardController(hid_device* dev_handle, const char* path, const std::string& device_name) +{ + dev = dev_handle; + location = path; + name = device_name; + supported = false; + via_protocol_version = 0; + + wchar_t serial_string[256]; + if(hid_get_serial_number_string(dev, serial_string, 256) == 0) + { + serial = StringUtils::wstring_to_string(serial_string); + } + + unsigned char version[2] = { 0, 0 }; + if(SendCommand(NUPHY_VIA_GET_PROTOCOL_VERSION, 0, 0, version, sizeof(version))) + { + via_protocol_version = (version[0] << 8) | version[1]; + } + + unsigned char brightness = 0; + supported = (via_protocol_version >= 9) + && SendCommand(NUPHY_VIA_CUSTOM_GET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_BRIGHTNESS, &brightness, sizeof(brightness)); +} + +NuPhyKeyboardController::~NuPhyKeyboardController() +{ + hid_close(dev); +} + +std::string NuPhyKeyboardController::GetLocation() +{ + return("HID: " + location); +} + +std::string NuPhyKeyboardController::GetName() +{ + return(name); +} + +std::string NuPhyKeyboardController::GetSerial() +{ + return(serial); +} + +std::string NuPhyKeyboardController::GetVersion() +{ + return("VIA: " + std::to_string(via_protocol_version)); +} + +bool NuPhyKeyboardController::GetSupported() +{ + return(supported); +} + +unsigned char NuPhyKeyboardController::GetBrightness() +{ + unsigned char value = 0; + SendCommand(NUPHY_VIA_CUSTOM_GET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_BRIGHTNESS, &value, sizeof(value)); + return(value); +} + +unsigned char NuPhyKeyboardController::GetEffect() +{ + unsigned char value = 0; + SendCommand(NUPHY_VIA_CUSTOM_GET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_EFFECT, &value, sizeof(value)); + return(value); +} + +unsigned char NuPhyKeyboardController::GetSpeed() +{ + unsigned char value = 0; + SendCommand(NUPHY_VIA_CUSTOM_GET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_SPEED, &value, sizeof(value)); + return(value); +} + +void NuPhyKeyboardController::GetColor(unsigned char* hue, unsigned char* saturation) +{ + unsigned char value[2] = { 0, 0 }; + SendCommand(NUPHY_VIA_CUSTOM_GET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_COLOR, value, sizeof(value)); + *hue = value[0]; + *saturation = value[1]; +} + +void NuPhyKeyboardController::SetBrightness(unsigned char brightness) +{ + SendCommand(NUPHY_VIA_CUSTOM_SET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_BRIGHTNESS, &brightness, sizeof(brightness)); +} + +void NuPhyKeyboardController::SetEffect(unsigned char effect) +{ + SendCommand(NUPHY_VIA_CUSTOM_SET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_EFFECT, &effect, sizeof(effect)); +} + +void NuPhyKeyboardController::SetSpeed(unsigned char speed) +{ + SendCommand(NUPHY_VIA_CUSTOM_SET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_SPEED, &speed, sizeof(speed)); +} + +void NuPhyKeyboardController::SetColor(unsigned char hue, unsigned char saturation) +{ + unsigned char value[2] = { hue, saturation }; + SendCommand(NUPHY_VIA_CUSTOM_SET_VALUE, NUPHY_VIA_RGB_MATRIX_CHANNEL, NUPHY_VIA_RGB_MATRIX_COLOR, value, sizeof(value)); +} + +void NuPhyKeyboardController::Save() +{ + SendCommand(NUPHY_VIA_CUSTOM_SAVE, NUPHY_VIA_RGB_MATRIX_CHANNEL, 0, nullptr, 0); +} + +bool NuPhyKeyboardController::SendCommand(unsigned char command, unsigned char channel, unsigned char value_id, unsigned char* value, unsigned char value_size) +{ + unsigned char usb_buf[NUPHY_VIA_MESSAGE_LENGTH + 1]; + memset(usb_buf, 0, sizeof(usb_buf)); + + usb_buf[1] = command; + usb_buf[2] = channel; + usb_buf[3] = value_id; + + if(value_size > 0) + { + memcpy(&usb_buf[4], value, value_size); + } + + if(hid_write(dev, usb_buf, sizeof(usb_buf)) < 0) + { + return(false); + } + + int bytes_received = hid_read_timeout(dev, usb_buf, NUPHY_VIA_MESSAGE_LENGTH, 1000); + if(bytes_received != NUPHY_VIA_MESSAGE_LENGTH || usb_buf[0] != command) + { + return(false); + } + + if(value_size > 0) + { + unsigned char response_offset = (command == NUPHY_VIA_GET_PROTOCOL_VERSION) ? 1 : 3; + memcpy(value, &usb_buf[response_offset], value_size); + } + + return(true); +} diff --git a/Controllers/NuPhyKeyboardController/NuPhyKeyboardController.h b/Controllers/NuPhyKeyboardController/NuPhyKeyboardController.h new file mode 100644 index 000000000..0bcf2825a --- /dev/null +++ b/Controllers/NuPhyKeyboardController/NuPhyKeyboardController.h @@ -0,0 +1,47 @@ +/*---------------------------------------------------------*\ +| NuPhyKeyboardController.h | +| | +| Driver for NuPhy QMK/VIA RGB Matrix keyboards | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include + +class NuPhyKeyboardController +{ +public: + NuPhyKeyboardController(hid_device* dev_handle, const char* path, const std::string& device_name); + ~NuPhyKeyboardController(); + + std::string GetLocation(); + std::string GetName(); + std::string GetSerial(); + std::string GetVersion(); + bool GetSupported(); + + unsigned char GetBrightness(); + unsigned char GetEffect(); + unsigned char GetSpeed(); + void GetColor(unsigned char* hue, unsigned char* saturation); + + void SetBrightness(unsigned char brightness); + void SetEffect(unsigned char effect); + void SetSpeed(unsigned char speed); + void SetColor(unsigned char hue, unsigned char saturation); + void Save(); + +private: + hid_device* dev; + std::string location; + std::string name; + std::string serial; + bool supported; + unsigned short via_protocol_version; + + bool SendCommand(unsigned char command, unsigned char channel, unsigned char value_id, unsigned char* value, unsigned char value_size); +}; diff --git a/Controllers/NuPhyKeyboardController/NuPhyKeyboardControllerDetect.cpp b/Controllers/NuPhyKeyboardController/NuPhyKeyboardControllerDetect.cpp new file mode 100644 index 000000000..b1c902e22 --- /dev/null +++ b/Controllers/NuPhyKeyboardController/NuPhyKeyboardControllerDetect.cpp @@ -0,0 +1,44 @@ +/*---------------------------------------------------------*\ +| NuPhyKeyboardControllerDetect.cpp | +| | +| Detector for NuPhy QMK/VIA RGB Matrix keyboards | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include "Detector.h" +#include "NuPhyKeyboardController.h" +#include "RGBController_NuPhyKeyboard.h" + +#define NUPHY_VID 0x19F5 +#define NUPHY_AIR75_V2_PID 0x3246 +#define NUPHY_AIR60_V2_PID 0x3255 +#define NUPHY_AIR96_V2_PID 0x3266 +#define QMK_VIA_USAGE_PAGE 0xFF60 +#define QMK_VIA_USAGE 0x0061 + +void DetectNuPhyKeyboardControllers(hid_device_info* info, const std::string& name) +{ + hid_device* dev = hid_open_path(info->path); + + if(dev) + { + NuPhyKeyboardController* controller = new NuPhyKeyboardController(dev, info->path, name); + + if(controller->GetSupported()) + { + RGBController_NuPhyKeyboard* rgb_controller = new RGBController_NuPhyKeyboard(controller); + ResourceManager::get()->RegisterRGBController(rgb_controller); + } + else + { + delete controller; + } + } +} + +REGISTER_HID_DETECTOR_PU("NuPhy Air60 V2", DetectNuPhyKeyboardControllers, NUPHY_VID, NUPHY_AIR60_V2_PID, QMK_VIA_USAGE_PAGE, QMK_VIA_USAGE); +REGISTER_HID_DETECTOR_PU("NuPhy Air75 V2", DetectNuPhyKeyboardControllers, NUPHY_VID, NUPHY_AIR75_V2_PID, QMK_VIA_USAGE_PAGE, QMK_VIA_USAGE); +REGISTER_HID_DETECTOR_PU("NuPhy Air96 V2", DetectNuPhyKeyboardControllers, NUPHY_VID, NUPHY_AIR96_V2_PID, QMK_VIA_USAGE_PAGE, QMK_VIA_USAGE); diff --git a/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp b/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp new file mode 100644 index 000000000..007031d89 --- /dev/null +++ b/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp @@ -0,0 +1,203 @@ +/*---------------------------------------------------------*\ +| RGBController_NuPhyKeyboard.cpp | +| | +| RGBController for NuPhy QMK/VIA RGB Matrix keyboards | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "hsv.h" +#include "RGBController_NuPhyKeyboard.h" + +typedef struct +{ + unsigned char value; + const char* name; + bool has_color; + bool has_speed; +} nuphy_mode; + +static const nuphy_mode nuphy_modes[] = +{ + { 0, "Off", false, false }, + { 1, "Static", true, false }, + { 2, "Alphas Mods", true, true }, + { 3, "Gradient Up Down", true, true }, + { 4, "Gradient Left Right", true, true }, + { 5, "Breathing", true, true }, + { 6, "Band Saturation", true, true }, + { 7, "Band Brightness", true, true }, + { 8, "Band Pinwheel Saturation", true, true }, + { 9, "Band Pinwheel Brightness", true, true }, + { 10, "Band Spiral Saturation", true, true }, + { 11, "Band Spiral Brightness", true, true }, + { 12, "Cycle All", false, true }, + { 13, "Cycle Left Right", false, true }, + { 14, "Cycle Up Down", false, true }, + { 15, "Rainbow Chevron", false, true }, + { 16, "Cycle Out In", false, true }, + { 17, "Cycle Out In Dual", false, true }, + { 18, "Cycle Pinwheel", false, true }, + { 19, "Cycle Spiral", false, true }, + { 20, "Dual Beacon", false, true }, + { 21, "Rainbow Beacon", false, true }, + { 22, "Rainbow Pinwheels", false, true }, + { 23, "Raindrops", false, true }, + { 24, "Jellybean Raindrops", false, true }, + { 25, "Hue Breathing", false, true }, + { 26, "Hue Pendulum", false, true }, + { 27, "Hue Wave", false, true }, + { 28, "Typing Heatmap", false, true }, + { 29, "Digital Rain", false, true }, + { 30, "Solid Reactive Simple", true, true }, + { 31, "Solid Reactive", true, true }, + { 32, "Solid Reactive Wide", true, true }, + { 33, "Solid Reactive Multiwide", true, true }, + { 34, "Solid Reactive Cross", true, true }, + { 35, "Solid Reactive Multicross", true, true }, + { 36, "Solid Reactive Nexus", true, true }, + { 37, "Solid Reactive Multinexus", true, true }, + { 38, "Splash", false, true }, + { 39, "Multisplash", false, true }, + { 40, "Solid Splash", true, true }, + { 41, "Solid Multisplash", true, true }, +}; + +/**------------------------------------------------------------------*\ + @name NuPhy Air V2 + @category Keyboard + @type USB + @save :white_check_mark: + @direct :x: + @effects :white_check_mark: + @detectors DetectNuPhyKeyboardControllers + @comment Supports the Air60 V2, Air75 V2, and Air96 V2 using their + stock QMK/VIA firmware. VIA does not expose direct per-key color + control, so OpenRGB controls whole-keyboard effects and colors. +\*-------------------------------------------------------------------*/ + +RGBController_NuPhyKeyboard::RGBController_NuPhyKeyboard(NuPhyKeyboardController* controller_ptr) +{ + controller = controller_ptr; + name = controller->GetName(); + description = "NuPhy QMK/VIA RGB Matrix Keyboard"; + vendor = "NuPhy"; + location = controller->GetLocation(); + serial = controller->GetSerial(); + version = controller->GetVersion(); + type = DEVICE_TYPE_KEYBOARD; + + unsigned char brightness = controller->GetBrightness(); + unsigned char speed = controller->GetSpeed(); + unsigned char hue = 0; + unsigned char saturation = 0; + controller->GetColor(&hue, &saturation); + + for(const nuphy_mode& effect : nuphy_modes) + { + mode new_mode; + new_mode.name = effect.name; + new_mode.value = effect.value; + new_mode.flags = MODE_FLAG_HAS_BRIGHTNESS; + new_mode.brightness_min = 0; + new_mode.brightness_max = 255; + new_mode.brightness = brightness; + + if(effect.has_speed) + { + new_mode.flags |= MODE_FLAG_HAS_SPEED; + new_mode.speed_min = 0; + new_mode.speed_max = 255; + new_mode.speed = speed; + } + + if(effect.has_color) + { + hsv_t hsv_color; + hsv_color.hue = (unsigned int)((float)hue * (360.0f / 256.0f)); + hsv_color.saturation = saturation; + hsv_color.value = 255; + + new_mode.flags |= MODE_FLAG_HAS_MODE_SPECIFIC_COLOR; + new_mode.color_mode = MODE_COLORS_MODE_SPECIFIC; + new_mode.colors_min = 1; + new_mode.colors_max = 1; + new_mode.colors.push_back(hsv2rgb(&hsv_color)); + } + + modes.push_back(new_mode); + } + + active_mode = controller->GetEffect(); + if(active_mode >= (int)modes.size()) + { + active_mode = 0; + } + + SetupZones(); +} + +RGBController_NuPhyKeyboard::~RGBController_NuPhyKeyboard() +{ + delete controller; +} + +void RGBController_NuPhyKeyboard::SetupZones() +{ + zone keyboard; + keyboard.name = "Keyboard"; + keyboard.type = ZONE_TYPE_SINGLE; + keyboard.leds_min = 1; + keyboard.leds_max = 1; + keyboard.leds_count = 1; + zones.push_back(keyboard); + + led keyboard_led; + keyboard_led.name = "Keyboard"; + leds.push_back(keyboard_led); + SetupColors(); +} + +void RGBController_NuPhyKeyboard::ResizeZone(int /*zone*/, int /*new_size*/) +{ +} + +void RGBController_NuPhyKeyboard::DeviceUpdateLEDs() +{ + if(!colors.empty()) + { + hsv_t hsv_color; + rgb2hsv(colors[0], &hsv_color); + controller->SetColor((unsigned char)((float)hsv_color.hue * (256.0f / 360.0f)), hsv_color.saturation); + } +} + +void RGBController_NuPhyKeyboard::UpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_NuPhyKeyboard::UpdateSingleLED(int /*led*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_NuPhyKeyboard::DeviceUpdateMode() +{ + controller->SetEffect((unsigned char)modes[active_mode].value); + controller->SetBrightness((unsigned char)modes[active_mode].brightness); + controller->SetSpeed((unsigned char)modes[active_mode].speed); + + if(modes[active_mode].color_mode == MODE_COLORS_MODE_SPECIFIC) + { + hsv_t hsv_color; + rgb2hsv(modes[active_mode].colors[0], &hsv_color); + controller->SetColor((unsigned char)((float)hsv_color.hue * (256.0f / 360.0f)), hsv_color.saturation); + } +} + +void RGBController_NuPhyKeyboard::DeviceSaveMode() +{ + controller->Save(); +} diff --git a/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.h b/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.h new file mode 100644 index 000000000..15b71b3f5 --- /dev/null +++ b/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.h @@ -0,0 +1,31 @@ +/*---------------------------------------------------------*\ +| RGBController_NuPhyKeyboard.h | +| | +| RGBController for NuPhy QMK/VIA RGB Matrix keyboards | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include "NuPhyKeyboardController.h" +#include "RGBController.h" + +class RGBController_NuPhyKeyboard : public RGBController +{ +public: + RGBController_NuPhyKeyboard(NuPhyKeyboardController* controller_ptr); + ~RGBController_NuPhyKeyboard(); + + void SetupZones(); + void ResizeZone(int zone, int new_size); + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + void DeviceUpdateMode(); + void DeviceSaveMode(); + +private: + NuPhyKeyboardController* controller; +}; From 576682be03600d7caa15628388a6442f5121c808 Mon Sep 17 00:00:00 2001 From: Gabriel Popa Date: Wed, 10 Jun 2026 15:38:11 +0200 Subject: [PATCH 2/3] Avoid near-stopped NuPhy animations --- .../RGBController_NuPhyKeyboard.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp b/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp index 007031d89..28dc31014 100644 --- a/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp +++ b/Controllers/NuPhyKeyboardController/RGBController_NuPhyKeyboard.cpp @@ -94,6 +94,15 @@ RGBController_NuPhyKeyboard::RGBController_NuPhyKeyboard(NuPhyKeyboardController unsigned char saturation = 0; controller->GetColor(&hue, &saturation); + /*-----------------------------------------------------*\ + | QMK speed zero advances animations so slowly that | + | effects such as Breathing appear stopped. | + \*-----------------------------------------------------*/ + if(speed == 0) + { + speed = 128; + } + for(const nuphy_mode& effect : nuphy_modes) { mode new_mode; From 793c28a039bca268ea5ab056d0a089b9490db445 Mon Sep 17 00:00:00 2001 From: Gabriel Popa Date: Wed, 10 Jun 2026 16:07:00 +0200 Subject: [PATCH 3/3] Avoid misleading SDK connection failure output --- NetworkClient.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkClient.cpp b/NetworkClient.cpp index a6a1b7c3e..3954db8dd 100644 --- a/NetworkClient.cpp +++ b/NetworkClient.cpp @@ -10,6 +10,7 @@ \*---------------------------------------------------------*/ #include +#include "LogManager.h" #include "NetworkClient.h" #include "RGBController_Network.h" @@ -267,7 +268,7 @@ void NetworkClient::ConnectionThreadFunction() } else { - printf( "Connection attempt failed\n" ); + LOG_DEBUG("[NetworkClient] Connection attempt failed"); } }