Ikea DIODER custom controller
Like lots of folks on the internet, I saw the DIODER and felt that it could be improved. And what better way than to make it internet-controlled? Given that I had a Nanode lying around looking for a use it seemed like providence.
So the plan was this:
- Instead of just cycling between some gaudy colours, lets pick some classier ones
- Let’s allow the colours to be updated via the web, saving them to EEPROM
- Using 1D Perlin noise to blend between a pair of colours will allow for pleasing fades and far more interest than just transitioning from A->B (so much interest that it turned out 2 colours is all it needs … for now)
- Use HSL colour space for super-classy fading
And here’s the code that does all that: rgb.pde.
The hardware side was pretty straightforward - a MOSFET (Stp36nf06l) per channel to buffer the output from the microcontroller (the LEDs run at 12v). I just used the 12v adaptor that came with the DIODER, and ditched the Ikea controller and little junction box thingy. I did add a switch to kick the ethernet chip into a low power mode (because it saves ~350mA and it was easy :))
It took a while to figure out the HSL colour space, so here’s what I ended up with:
// based on http://www.dipzo.com/wordpress/?p=50
void hslToRgb( int hue, byte sat, byte light, byte* lpOutRgb )
{
if( sat == 0 )
{
lpOutRgb[0] = lpOutRgb[1] = lpOutRgb[2] = light;
return;
}
float nhue = (float)hue * (1.0f / 360.0f);
float nsat = (float)sat * (1.0f / 255.0f);
float nlight = (float)light * (1.0f / 255.0f);
float m2;
if( light < 128 )
m2 = nlight * ( 1.0f + nsat );
else
m2 = ( nlight + nsat ) - ( nsat * nlight );
float m1 = ( 2.0f * nlight ) - m2;
lpOutRgb[0] = hueToChannel( m1, m2, nhue + (1.0f / 3.0f) );
lpOutRgb[1] = hueToChannel( m1, m2, nhue );
lpOutRgb[2] = hueToChannel( m1, m2, nhue - (1.0f / 3.0f) );
}
byte hueToChannel( float m1, float m2, float h )
{
float channel = hueToChannelInternal( m1, m2, h );
byte uchan = (byte)(255.0f * channel);
return uchan;
}
float hueToChannelInternal( float m1, float m2, float h )
{
if( h < 0.0f ) h += 1.0f;
if( h > 1.0f ) h -= 1.0f;
if( ( 6.0f * h ) < 1.0f ) return ( m1 + ( m2 - m1 ) * 6.0f * h );
if( ( 2.0f * h ) < 1.0f ) return m2;
if( ( 3.0f * h ) < 2.0f ) return ( m1 + ( m2 - m1 ) * ((2.0f/3.0f) - h) * 6.0f );
return m1;
}
I also experimented with adding a gamma curve to the output, but while it definitely improved colour matching, it made fading a lot more jerky, so I didn’t go with it in the end.
Once I had reliable HSL->RGB working, I needed a good HSL interpolate function. That took some effort. I found you can use a straight linear interpolate on the S & L channels, but you need a custom one for the Hue channel as it needs to pick the shortest way to go around the circle. I also wrote it all to treat the interpolation parameter as a value from 0 to 1 in 0.8 fixed point format. This is how it looks:
void lerpHsl( const int* a, const int* b, byte t, int* lpOutHsl )
{
lpOutHsl[0] = lerpHueFp8( a[0], b[0], t );
lpOutHsl[1] = lerpFp8( a[1], b[1], t );
lpOutHsl[2] = lerpFp8( a[2], b[2], t );
}
int lerpFp8( int a, int b, byte t )
{
long t0 = ( b - a ) * t;
return ( a + ( t0 >> 8 ) );
}
int lerpHueFp8( int a, int b, byte t )
{
// adjust inputs so we take the shortest route around the circle
int cwdist = b - a;
if( abs(cwdist) > 180 )
{
if( b > a ) a += 360;
else b += 360;
}
long t0 = ( b - a ) * t;
int l = a + (t0 >> 8);
return ( l % 360 );
}
The last colour-related piece was to get a good 1D Perlin noise-like function. After some googling failed to turn up a nice implementation for Arduino, I built this, based on Hugo Elias’ great page here.
#define kPerlinOctaves 3
static const float kaPerlinOctaveAmplitude[kPerlinOctaves] =
{
0.75f, 0.4f, 0.1f,
};
float perlinNoise1( long x )
{
x = ( x<<13 ) ^ x;
return ( 1.0f - ( (x * (x * x * 15731 + 789221L) + 1376312589L) & 0x7fffffff) * (1.0f / 1073741824.0f) );
}
float perlinSmoothedNoise1( long x )
{
return perlinNoise1(x)*0.5f
+ perlinNoise1(x-1)*0.25f
+ perlinNoise1(x+1)*0.25f;
}
float perlinLerpedNoise1( float x )
{
long xInteger = long(x);
float xFraction = x - xInteger;
float v1 = perlinSmoothedNoise1( xInteger );
float v2 = perlinSmoothedNoise1( xInteger + 1 );
return lerpFloat( v1, v2, xFraction );
}
float perlinNoise1D( float x )
{
float total = 0.0f;
for( int octave = 0; octave < kPerlinOctaves; octave++ )
{
int frequency = 1 << octave;
float amplitude = kaPerlinOctaveAmplitude[octave];
total += perlinLerpedNoise1( x * frequency ) * amplitude;
}
// map from [-1,1] to [0,1] and constrain
return constrain( (total + 1.0f) * 0.5f, 0.0f, 1.0f );
}
This generates the following output, which looks pretty sweet when used as the interpolation parameter
With that, all of the RGB LED controlling was done :)
For the web interface, I used the excellent EtherCard library, which made it super-easy.
Job done*!
*actually, I still need to build a nice jQuery’d webpage to make setting colours easier. Currently I need to go to http://192.168.1.25/hsl?i=0&h=135&s=90&l=50, which isn’t exactly as futuristic as I’d like :)