Skip to content

Commit

Permalink
🐛 Fix Chroma Dock lighting effects.
Browse files Browse the repository at this point in the history
It turns out the set of effect IDs were also different. Implementation only seemed to work superficially because some effect IDs were the same or similar ebough.
This adds another effect enum with IDs corresponding to the actual supported effects.
  • Loading branch information
hexawyz committed Sep 28, 2024
1 parent 07bc62e commit e4191e9
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 83 deletions.
37 changes: 34 additions & 3 deletions Docs/Razer DeathAdder Platform 3.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Razer Platform 3

Platform 2 and Platform 3 are the names given by Razer for this protocol used for Chroma devices and newer. (Also saw a reference to "Protocol 2.5" in the files 🤷)
Platform 3 (and Platform 2?) is the name given by Razer for this protocol used for Chroma devices and newer. (Also saw a reference to "Protocol 2.5" in the files 🤷)
This protocol uses 90 byte packets sent and received through HID Feature GET/SET on report ID `00`.

Razer generally installs kernel drivers to access those devices, although their use does not seem to be strictly necessary.
Expand Down Expand Up @@ -492,6 +492,26 @@ Write:
<Persist:U8> <LedId:U8> <Effect:U8>
```

This is used to set effects on the Mamba Chroma Dock.
The effect IDs used here seem (again) different from the other effect IDs ☹️

###### Effect List

0. Static
1. Blinking with one color (Not visible in Synapse)
2. Breathing with one color (Buggy)
3. Breathing starting with static color ? (Not visible in Synapse)
4. Spectrum Cycle
5. Breathing with two colors

NB: The single color breathing effect seem to be buggy:

From what I understand, it is supposed to take its color from the LED Color, but it seems like this will be overridden by extended breathing parameters somehow?

i.e. In Synapse, I would see the static color be ignored and the first one from the extended parameters be used. (Might be a result of me twiddling with the device before, but this is weird)

But as such, there is a quick fix: Just also override the extended breathing parameters when choosing the single color breathing.

##### Function `03`:`03` - Brightness

Read Request:
Expand Down Expand Up @@ -539,9 +559,20 @@ Read Request:
Write:

```
<Persist:U8> <LedId:U8> <Values:U6[9]>
<Persist:U8> <LedId:U8> <Values:U16[9]>
```

Not sure about this one, but some of the values might be:

PWMOffTime
PWMOnTime
PWMRiseSteps
PWMFallSteps
PWMRiseStepTime
PWMFallStepTime
TmpMax
TmpMin

##### Function `03`:`07` - ???

Read Request:
Expand Down Expand Up @@ -586,7 +617,7 @@ When setting an effect on the Mamba Chroma Dock:
00 3f 000000 09030e 01 0f 02 00ffff ff00ff 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800
```

This sets the breathing effect parameters for the specified led. It does not enable the breathing effect, which needs to be enabeld separately.
This sets the breathing effect parameters for the specified led. It does not enable the breathing effect, which needs to be enabled separately.

##### Function `03`:`0F` - Dock Lighting Synchronization

Expand Down
17 changes: 17 additions & 0 deletions src/Exo/Core/Exo.Core/Lighting/Effects/ColorBlinkEffect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Exo.ColorFormats;

namespace Exo.Lighting.Effects;

/// <summary>Represents a light with a blinking color effect.</summary>
/// <remarks>This is similar to the flash effect, except that the on and off states have the same period.</remarks>
[TypeId(0x46BE7D40, 0x8A97, 0x486C, 0x97, 0xCD, 0x07, 0xE9, 0x31, 0xF4, 0x6A, 0xB9)]
public readonly struct ColorBlinkEffect : ISingleColorLightEffect
{
[DataMember(Order = 1)]
[Display(Name = "Color")]
public RgbColor Color { get; }

public ColorBlinkEffect(RgbColor color) => Color = color;
}
6 changes: 3 additions & 3 deletions src/Exo/Devices/Exo.Devices.Razer/IRazerProtocolTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ internal interface IRazerProtocolTransport : IDisposable
ValueTask<RgbColor> GetStaticColorV1Async(RazerLedId ledId, CancellationToken cancellationToken);
Task SetBrightnessV1Async(RazerLedId ledId, byte value, CancellationToken cancellationToken);
ValueTask<byte> GetBrightnessV1Async(RazerLedId ledId, CancellationToken cancellationToken);
Task SetEffectV1Async(RazerLedId ledId, RazerLightingEffectV1 effect, CancellationToken cancellationToken);
ValueTask<RazerLightingEffectV1> GetEffectV1Async(RazerLedId ledId, CancellationToken cancellationToken);
Task SetBreathingEffectParametersV1Async(RazerLedId ledId, CancellationToken cancellationToken);
Task SetEffectV1Async(RazerLedId ledId, RazerLightingEffectV0 effect, CancellationToken cancellationToken);
ValueTask<RazerLightingEffectV0> GetEffectV1Async(RazerLedId ledId, CancellationToken cancellationToken);
//Task SetBreathingEffectParametersV1Async(RazerLedId ledId, CancellationToken cancellationToken);
Task SetBreathingEffectParametersV1Async(RazerLedId ledId, RgbColor color, CancellationToken cancellationToken);
Task SetBreathingEffectParametersV1Async(RazerLedId ledId, RgbColor color1, RgbColor color2, CancellationToken cancellationToken);
ValueTask<(byte ColorCount, RgbColor Color1, RgbColor Color2)> GetBreathingEffectParametersV1Async(RazerLedId ledId, CancellationToken cancellationToken);
Expand Down
4 changes: 4 additions & 0 deletions src/Exo/Devices/Exo.Devices.Razer/LightingEffects.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
// Exo.Devices.Razer.LightingEffects.SynchronizedEffect
"7b58f6d8-e492-452c-a747-43d1a9c46177": {
"nameStringId": "62492f89-62cf-4d1b-af66-875b3795cb4d"
},
// Exo.Devices.Razer.LightingEffects.TwoColorPulseEffect
"45e154d6-1946-4215-a44f-975cb76deeae": {
"nameStringId": "5cf7ee5d-b9be-430b-b083-ca5a21dc9a07"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -948,9 +948,9 @@ static unsafe void WriteData(SafeFileHandle serviceHandle, in BluetoothLeCharact
ValueTask<bool> IRazerProtocolTransport.IsLedEnabledV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
Task IRazerProtocolTransport.SetStaticColorV1Async(RazerLedId ledId, RgbColor color, CancellationToken cancellationToken) => throw new NotSupportedException();
ValueTask<RgbColor> IRazerProtocolTransport.GetStaticColorV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
Task IRazerProtocolTransport.SetEffectV1Async(RazerLedId ledId, RazerLightingEffectV1 effect, CancellationToken cancellationToken) => throw new NotSupportedException();
ValueTask<RazerLightingEffectV1> IRazerProtocolTransport.GetEffectV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
Task IRazerProtocolTransport.SetBreathingEffectParametersV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
Task IRazerProtocolTransport.SetEffectV1Async(RazerLedId ledId, RazerLightingEffectV0 effect, CancellationToken cancellationToken) => throw new NotSupportedException();
ValueTask<RazerLightingEffectV0> IRazerProtocolTransport.GetEffectV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
//Task IRazerProtocolTransport.SetBreathingEffectParametersV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
Task IRazerProtocolTransport.SetBreathingEffectParametersV1Async(RazerLedId ledId, RgbColor color, CancellationToken cancellationToken) => throw new NotSupportedException();
Task IRazerProtocolTransport.SetBreathingEffectParametersV1Async(RazerLedId ledId, RgbColor color1, RgbColor color2, CancellationToken cancellationToken) => throw new NotSupportedException();
ValueTask<(byte ColorCount, RgbColor Color1, RgbColor Color2)> IRazerProtocolTransport.GetBreathingEffectParametersV1Async(RazerLedId ledId, CancellationToken cancellationToken) => throw new NotSupportedException();
Expand Down
94 changes: 26 additions & 68 deletions src/Exo/Devices/Exo.Devices.Razer/RazerDeviceDriver.BaseDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ internal void SetBrightness(byte brightness)

protected class LightingZoneV1 : LightingZone,
ILightingZoneEffect<StaticColorEffect>,
ILightingZoneEffect<RandomColorPulseEffect>,
ILightingZoneEffect<ColorBlinkEffect>,
ILightingZoneEffect<ColorPulseEffect>,
ILightingZoneEffect<TwoColorPulseEffect>,
ILightingZoneEffect<SpectrumCycleEffect>,
Expand Down Expand Up @@ -145,24 +145,27 @@ protected override Task ApplyBrightnessAsync(byte profileId, byte brightness, Ca

switch (await Transport.GetEffectV1Async(_ledId, cancellationToken).ConfigureAwait(false))
{
case RazerLightingEffectV1.Disabled: return DisabledEffect.SharedInstance;
case RazerLightingEffectV1.Wave: return SpectrumWaveEffect.SharedInstance;
case RazerLightingEffectV1.Reactive: /* TODO*/ break;
case RazerLightingEffectV1.Breathing:
case RazerLightingEffectV0.Static:
return new StaticColorEffect(await Transport.GetStaticColorV1Async(_ledId, cancellationToken).ConfigureAwait(false));
case RazerLightingEffectV0.Blink:
return new ColorBlinkEffect(await Transport.GetStaticColorV1Async(_ledId, cancellationToken).ConfigureAwait(false));
case RazerLightingEffectV0.Breathing:
return new ColorPulseEffect(await Transport.GetStaticColorV1Async(_ledId, cancellationToken).ConfigureAwait(false));
case RazerLightingEffectV0.BreathingExtended:
try
{
var (colorCount, color1, color2) = await Transport.GetBreathingEffectParametersV1Async(_ledId, cancellationToken).ConfigureAwait(false);
if (colorCount == 2) return new TwoColorPulseEffect(color1, color2);
else if (colorCount == 1) return new ColorPulseEffect(color1);
else return RandomColorPulseEffect.SharedInstance;
else return DisabledEffect.SharedInstance;
}
catch
{
// It might not always be possible read back this parameter for some reason?? (Or I made a mistake in the read method maybe?)
return RandomColorPulseEffect.SharedInstance;
return DisabledEffect.SharedInstance;
}
case RazerLightingEffectV1.Static:
return new StaticColorEffect(await Transport.GetStaticColorV1Async(_ledId, cancellationToken).ConfigureAwait(false));
case RazerLightingEffectV0.SpectrumCycle:
return SpectrumCycleEffect.SharedInstance;
}

// This is a lie, but default to reporting the disabled effect when read an effect that we don't support.
Expand All @@ -181,7 +184,7 @@ protected override async Task ApplyEffectAsync(byte profileId, ILightingEffect e
bool shouldDisableSynchronization = true;
bool shouldChangeCurrentEffect = true;

RazerLightingEffectV1 effectId = RazerLightingEffectV1.Disabled;
RazerLightingEffectV0 effectId = RazerLightingEffectV0.Static;

switch (effect)
{
Expand All @@ -191,28 +194,23 @@ protected override async Task ApplyEffectAsync(byte profileId, ILightingEffect e
break;
case StaticColorEffect staticColorEffect:
await transport.SetStaticColorV1Async(_ledId, staticColorEffect.Color, cancellationToken);
effectId = RazerLightingEffectV1.Static;
effectId = RazerLightingEffectV0.Static;
break;
case RandomColorPulseEffect:
await transport.SetBreathingEffectParametersV1Async(_ledId, cancellationToken);
effectId = RazerLightingEffectV1.Breathing;
case ColorBlinkEffect blinkEffect:
await transport.SetStaticColorV1Async(_ledId, blinkEffect.Color, cancellationToken);
effectId = RazerLightingEffectV0.Blink;
break;
case ColorPulseEffect colorPulseEffect:
await transport.SetBreathingEffectParametersV1Async(_ledId, colorPulseEffect.Color, cancellationToken);
effectId = RazerLightingEffectV1.Breathing;
await transport.SetStaticColorV1Async(_ledId, colorPulseEffect.Color, cancellationToken);
effectId = RazerLightingEffectV0.Breathing;
break;
case TwoColorPulseEffect twoColorPulseEffect:
await transport.SetBreathingEffectParametersV1Async(_ledId, twoColorPulseEffect.Color, twoColorPulseEffect.SecondColor, cancellationToken);
effectId = RazerLightingEffectV1.Breathing;
effectId = RazerLightingEffectV0.BreathingExtended;
break;
case SpectrumCycleEffect:
effectId = RazerLightingEffectV1.SpectrumCycle;
break;
case SpectrumWaveEffect:
effectId = RazerLightingEffectV1.Wave;
break;
case ReactiveEffect:
effectId = RazerLightingEffectV1.Reactive;
effectId = RazerLightingEffectV0.SpectrumCycle;
break;
case SynchronizedEffect:
await transport.SetSynchronizedLightingV1Async(_ledId, true, cancellationToken);
Expand Down Expand Up @@ -241,8 +239,8 @@ protected override async Task ApplyEffectAsync(byte profileId, ILightingEffect e
void ILightingZoneEffect<StaticColorEffect>.ApplyEffect(in StaticColorEffect effect) => SetCurrentEffect(effect);
bool ILightingZoneEffect<StaticColorEffect>.TryGetCurrentEffect(out StaticColorEffect effect) => CurrentEffect.TryGetEffect(out effect);

void ILightingZoneEffect<RandomColorPulseEffect>.ApplyEffect(in RandomColorPulseEffect effect) => SetCurrentEffect(effect);
bool ILightingZoneEffect<RandomColorPulseEffect>.TryGetCurrentEffect(out RandomColorPulseEffect effect) => CurrentEffect.TryGetEffect(out effect);
void ILightingZoneEffect<ColorBlinkEffect>.ApplyEffect(in ColorBlinkEffect effect) => SetCurrentEffect(effect);
bool ILightingZoneEffect<ColorBlinkEffect>.TryGetCurrentEffect(out ColorBlinkEffect effect) => CurrentEffect.TryGetEffect(out effect);

void ILightingZoneEffect<ColorPulseEffect>.ApplyEffect(in ColorPulseEffect effect) => SetCurrentEffect(effect);
bool ILightingZoneEffect<ColorPulseEffect>.TryGetCurrentEffect(out ColorPulseEffect effect) => CurrentEffect.TryGetEffect(out effect);
Expand All @@ -264,42 +262,6 @@ public DockLightingZoneV1(BaseDevice device, Guid zoneId, RazerLedId ledId) : ba
bool ILightingZoneEffect<SynchronizedEffect>.TryGetCurrentEffect(out SynchronizedEffect effect) => CurrentEffect.TryGetEffect(out effect);
}

protected class WaveLightingZoneV1 : LightingZoneV1, ILightingZoneEffect<ColorWaveEffect>
{
public WaveLightingZoneV1(BaseDevice device, Guid zoneId, RazerLedId ledId) : base(device, zoneId, ledId)
{
}

void ILightingZoneEffect<ColorWaveEffect>.ApplyEffect(in ColorWaveEffect effect) => SetCurrentEffect(effect);
bool ILightingZoneEffect<ColorWaveEffect>.TryGetCurrentEffect(out ColorWaveEffect effect) => CurrentEffect.TryGetEffect(out effect);
}

protected class WaveReactiveLightingZoneV1 : WaveLightingZoneV1, ILightingZoneEffect<ReactiveEffect>
{
public WaveReactiveLightingZoneV1(BaseDevice device, Guid zoneId, RazerLedId ledId) : base(device, zoneId, ledId)
{
}

void ILightingZoneEffect<ReactiveEffect>.ApplyEffect(in ReactiveEffect effect) => SetCurrentEffect(effect);
bool ILightingZoneEffect<ReactiveEffect>.TryGetCurrentEffect(out ReactiveEffect effect) => CurrentEffect.TryGetEffect(out effect);
}

// NB: Naming sucks, but basically, this and the one below are zones implemented as non-unified, but exposed as unified.
// (Because we need to control the lighting of Chroma mamba without affecting the lighting of the dock)
protected sealed class SingleWaveLightingZoneV1 : WaveReactiveLightingZoneV1, IUnifiedLightingFeature
{
public SingleWaveLightingZoneV1(BaseDevice device, Guid zoneId, RazerLedId ledId) : base(device, zoneId, ledId)
{
}
}

protected sealed class SingleWaveReactiveLightingZoneV1 : WaveReactiveLightingZoneV1, IUnifiedLightingFeature
{
public SingleWaveReactiveLightingZoneV1(BaseDevice device, Guid zoneId, RazerLedId ledId) : base(device, zoneId, ledId)
{
}
}

protected abstract class UnifiedLightingZoneV1 : LightingZone
{
private readonly RazerLedId _mainLedId;
Expand Down Expand Up @@ -619,13 +581,9 @@ protected virtual (LightingZone? UnifiedLightingZone, ImmutableArray<LightingZon
}
else
{
unifiedLightingZone = deviceInformation.UseNonUnifiedLightingAsUnified ?
deviceInformation.HasReactiveLighting ?
new SingleWaveReactiveLightingZoneV1(this, lightingZoneGuid, deviceInformation.MainLedId) :
new SingleWaveLightingZoneV1(this, lightingZoneGuid, deviceInformation.MainLedId) :
deviceInformation.HasReactiveLighting ?
new ReactiveUnifiedLightingZoneV1(this, lightingZoneGuid, deviceInformation.MainLedId) :
new BasicUnifiedLightingZoneV1(this, lightingZoneGuid, deviceInformation.MainLedId);
unifiedLightingZone = deviceInformation.HasReactiveLighting ?
new ReactiveUnifiedLightingZoneV1(this, lightingZoneGuid, deviceInformation.MainLedId) :
new BasicUnifiedLightingZoneV1(this, lightingZoneGuid, deviceInformation.MainLedId);
}
}
return (unifiedLightingZone, []);
Expand Down
10 changes: 10 additions & 0 deletions src/Exo/Devices/Exo.Devices.Razer/RazerLightingEffectV0.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Exo.Devices.Razer;

public enum RazerLightingEffectV0 : byte
{
Static = 0,
Blink = 1,
Breathing = 2,
SpectrumCycle = 4,
BreathingExtended = 5,
}
3 changes: 2 additions & 1 deletion src/Exo/Devices/Exo.Devices.Razer/RazerLightingEffectV1.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Exo.Devices.Razer;
namespace Exo.Devices.Razer;

public enum RazerLightingEffectV1 : byte
{
Expand All @@ -7,5 +7,6 @@ public enum RazerLightingEffectV1 : byte
Reactive = 2,
Breathing = 3,
SpectrumCycle = 4,
Dynamic = 5,
Static = 6,
}
10 changes: 5 additions & 5 deletions src/Exo/Devices/Exo.Devices.Razer/RazerProtocolTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,15 +324,15 @@ static RgbColor ParseResult(ReadOnlySpan<byte> buffer)
}
}

public async Task SetEffectV1Async(RazerLedId ledId, RazerLightingEffectV1 effect, CancellationToken cancellationToken)
public async Task SetEffectV1Async(RazerLedId ledId, RazerLightingEffectV0 effect, CancellationToken cancellationToken)
{
var @lock = Volatile.Read(ref _lock);
ObjectDisposedException.ThrowIf(@lock is null, typeof(RazerProtocolTransport));
using (await @lock.WaitAsync(cancellationToken).ConfigureAwait(false))
{
var buffer = Buffer;

static void FillBuffer(Span<byte> buffer, RazerLedId ledId, RazerLightingEffectV1 effect)
static void FillBuffer(Span<byte> buffer, RazerLedId ledId, RazerLightingEffectV0 effect)
{
WriteRequestHeader(buffer, 0x1f, RazerDeviceFeature.LightingV1, 0x02, 0x03);

Expand All @@ -358,7 +358,7 @@ static void FillBuffer(Span<byte> buffer, RazerLedId ledId, RazerLightingEffectV
}
}

public async ValueTask<RazerLightingEffectV1> GetEffectV1Async(RazerLedId ledId, CancellationToken cancellationToken)
public async ValueTask<RazerLightingEffectV0> GetEffectV1Async(RazerLedId ledId, CancellationToken cancellationToken)
{
var @lock = Volatile.Read(ref _lock);
ObjectDisposedException.ThrowIf(@lock is null, typeof(RazerProtocolTransport));
Expand All @@ -376,8 +376,8 @@ static void FillBuffer(Span<byte> buffer, RazerLedId ledId)
UpdateChecksum(buffer);
}

static RazerLightingEffectV1 ParseResult(ReadOnlySpan<byte> buffer)
=> (RazerLightingEffectV1)buffer[2];
static RazerLightingEffectV0 ParseResult(ReadOnlySpan<byte> buffer)
=> (RazerLightingEffectV0)buffer[2];

try
{
Expand Down
4 changes: 4 additions & 0 deletions src/Exo/Devices/Exo.Devices.Razer/Strings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"62492f89-62cf-4d1b-af66-875b3795cb4d": {
"en": "Synchronized",
"fr": "Synchronisé"
},
"5cf7ee5d-b9be-430b-b083-ca5a21dc9a07": {
"en": "Two Color Pulses",
"fr": "Pulsations de deux couleurs alternées"
Expand Down
4 changes: 4 additions & 0 deletions src/Exo/Service/Exo.Service/LightingEffects.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"d575547b-f8b0-4f80-98df-e1a9112c7270": {
"nameStringId": "f49c1855-5583-4a4f-8272-7f0326a23197"
},
// Exo.Lighting.Effects.ColorBlinkEffect
"46be7d40-8a97-486c-97cd-07e931f46ab9": {
"nameStringId": "965a4868-b74e-4674-b43f-2086daf37906"
},
// Exo.Lighting.Effects.ColorFlashEffect
"a3f80010-5663-4ecf-9c22-036e28478b7e": {
"nameStringId": "419e70ee-8355-4cf5-9d18-54835008cdb0"
Expand Down
4 changes: 4 additions & 0 deletions src/Exo/Service/Exo.Service/Strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@
"en": "Pulse (Speed)",
"fr": "Pulsation (Vitesse)"
},
"965a4868-b74e-4674-b43f-2086daf37906": {
"en": "BLink",
"fr": "Clignottement"
},
"419e70ee-8355-4cf5-9d18-54835008cdb0": {
"en": "Flash",
"fr": "Flashs"
Expand Down

0 comments on commit e4191e9

Please sign in to comment.