Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"react-slider": "^1.1.2",
"react-tabs": "^3.1.2",
"source-map-support": "^0.5.19",
"tinycolor2": "^1.4.2"
"tinycolor2": "^1.4.2",
"usb": "^2.11.0"
},
"devDependencies": {
"@babel/preset-react": "^7.14.5",
Expand Down
6 changes: 6 additions & 0 deletions src/main/razerapplication.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SettingsManager } from './settingsmanager';
import { RazerAnimationCycleSpectrum } from './animation/animationcyclespectrum';
import { RazerAnimationCycleCustom } from './animation/animationcyclecustom';
import { StateManager } from './statemanager';
import { USBMonitor } from './usbmonitor';

/**
* Main application
Expand All @@ -17,8 +18,12 @@ export class RazerApplication {
this.settingsManager = new SettingsManager();
this.stateManager = new StateManager(this.settingsManager);
this.deviceManager = new RazerDeviceManager(this.settingsManager, this.stateManager);
this.usbMonitor = new USBMonitor(this.deviceManager, this.stateManager);
this.spectrumAnimation = null;
this.cycleAnimation = null;

// Start USB monitoring for KVM support
this.usbMonitor.start();
}

async refresh(withOnStartState = true) {
Expand All @@ -35,6 +40,7 @@ export class RazerApplication {
}

destroy() {
this.usbMonitor.stop();
this.deviceManager.destroy();
}

Expand Down
30 changes: 30 additions & 0 deletions src/main/statemanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class StateManager {
this.stateOnUnlockScreen = savedState.stateOnUnlockScreen;
this.stateOnUserDidBecomeActive = savedState.stateOnUserDidBecomeActive;
this.stateOnUserDidResignActive = savedState.stateOnUserDidResignActive;
this.stateOnDeviceReconnect = savedState.stateOnDeviceReconnect || null;
} else {
await this.createNewState();
}
Expand All @@ -49,6 +50,7 @@ export class StateManager {
this.stateOnUnlockScreen = null;
this.stateOnUserDidBecomeActive = null;
this.stateOnUserDidResignActive = null;
this.stateOnDeviceReconnect = null; // KVM: State to apply when device reconnects
await this.save();
}

Expand Down Expand Up @@ -115,6 +117,7 @@ export class StateManager {
stateOnUnlockScreen: this.stateOnUnlockScreen,
stateOnUserDidBecomeActive: this.stateOnUserDidBecomeActive,
stateOnUserDidResignActive: this.stateOnUserDidResignActive,
stateOnDeviceReconnect: this.stateOnDeviceReconnect,
});
}

Expand Down Expand Up @@ -207,6 +210,32 @@ export class StateManager {
}
}

async deviceReconnect() {
return this.changeToState(this.stateOnDeviceReconnect);
}

/**
* Apply the current state to all devices
* Used when device reconnects via KVM to restore lighting
*/
async applyCurrentState() {
// If there's a specific state for device reconnect, use it
if (this.stateOnDeviceReconnect) {
await this.deviceReconnect();
return;
}

// Otherwise, apply the current state of each device
if (this.devices) {
for (const device of this.devices) {
const currentState = device.getState();
if (currentState) {
device.resetToState(currentState);
}
}
}
}

serialize() {
return {
devices: this.devices.map(device => device.serialize()),
Expand All @@ -221,6 +250,7 @@ export class StateManager {
stateOnUnlockScreen: this.stateOnUnlockScreen,
stateOnUserDidBecomeActive: this.stateOnUserDidBecomeActive,
stateOnUserDidResignActive: this.stateOnUserDidResignActive,
stateOnDeviceReconnect: this.stateOnDeviceReconnect,
};
}
}
136 changes: 136 additions & 0 deletions src/main/usbmonitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import usb from 'usb';

/**
* USB Device Monitor for detecting Razer device connections/disconnections
* Used for KVM setup - auto-refresh when devices reconnect
*/
export class USBMonitor {
constructor(deviceManager, stateManager) {
this.deviceManager = deviceManager;
this.stateManager = stateManager;
this.isMonitoring = false;
this.razerVendorIds = [0x1532]; // Razer vendor ID
this.detachedDevices = new Set();
}

/**
* Start monitoring USB events
*/
start() {
if (this.isMonitoring) {
return;
}

this.isMonitoring = true;

// Listen for device attachment
usb.on('attach', (device) => {
this.handleDeviceAttach(device);
});

// Listen for device detachment
usb.on('detach', (device) => {
this.handleDeviceDetach(device);
});

console.log('[USBMonitor] Started monitoring USB events');
}

/**
* Stop monitoring USB events
*/
stop() {
if (!this.isMonitoring) {
return;
}

usb.removeAllListeners('attach');
usb.removeAllListeners('detach');
this.isMonitoring = false;

console.log('[USBMonitor] Stopped monitoring USB events');
}

/**
* Handle USB device attachment
*/
handleDeviceAttach(device) {
const vendorId = device.deviceDescriptor.idVendor;
const productId = device.deviceDescriptor.idProduct;

// Check if it's a Razer device
if (this.isRazerDevice(vendorId)) {
console.log(`[USBMonitor] Razer device attached: ${productId.toString(16)}`);

// Check if this device was previously detached (KVM scenario)
const deviceKey = `${vendorId}-${productId}`;
const wasDetached = this.detachedDevices.has(deviceKey);

// Wait a moment for the device to be ready
setTimeout(() => {
this.refreshAndRestore(deviceKey, wasDetached);
}, 1000);

// Remove from detached list
this.detachedDevices.delete(deviceKey);
}
}

/**
* Handle USB device detachment
*/
handleDeviceDetach(device) {
const vendorId = device.deviceDescriptor.idVendor;
const productId = device.deviceDescriptor.idProduct;

// Check if it's a Razer device
if (this.isRazerDevice(vendorId)) {
console.log(`[USBMonitor] Razer device detached: ${productId.toString(16)}`);

// Track this device as detached
const deviceKey = `${vendorId}-${productId}`;
this.detachedDevices.add(deviceKey);
}
}

/**
* Check if device is from Razer
*/
isRazerDevice(vendorId) {
return this.razerVendorIds.includes(vendorId);
}

/**
* Refresh devices and restore lighting
*/
async refreshAndRestore(deviceKey, wasDetached) {
try {
console.log('[USBMonitor] Refreshing device list...');

// Refresh the device list
await this.deviceManager.refreshRazerDevices();

// If this was a reconnection (KVM scenario), restore the lighting
if (wasDetached && this.deviceManager.activeRazerDevices) {
console.log('[USBMonitor] Restoring lighting for reconnected devices');

// Small delay to ensure devices are fully initialized
await new Promise(resolve => setTimeout(resolve, 500));

// Apply current state to all active devices
if (this.stateManager) {
await this.stateManager.applyCurrentState();
}
}
} catch (error) {
console.error('[USBMonitor] Error during refresh:', error);
}
}

/**
* Check if currently monitoring
*/
get isActive() {
return this.isMonitoring;
}
}