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.

Finished DIODER controller

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 :))

 

closeup of the controller board
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 🙂

Brighton Mini Maker Faire

Maker Faire came to Brighton!

Spent a very happy hour marvelling at all the amazing things people had made. Awesome 🙂

Loads more pics and videos here: makerfairebrighton.tumblr.com