Milight bulbs and Milight remote experiments are what this page is all about. These are sold not only under the Milight brand but also as Easybulb and LimitlessLED. This page focusses on the RF method of communication with the bulbs. RF is the way that the milight bridge communicates with the bulb also.

I used the Milight 6W RGBWW bulb costing €7.98 and this remote control suitable for the RGBW bulb which costs €7.13. In addition i have bought two CW WW remotes which cost €6,47. Opening the bulb you find a PL1167 RF transmitter/receiver and the STM8S003F3P6 microcontroller. After buying these i found that there was a new development with additional possibilities. These are this remote costing €8.48 and this 8W RGBWW & CW bulb costing €14.33. The difference is that  there is saturation control and a colour temperature control. There is a separate page dedicated to this type as they are very different from the other remotes due to the encryption.

 milight bulbmilight-cwww-remote

I wanted to see if it would be possible to mimic the Philips Hue by adding more functionality to the milight bulbs. To do that the first steps are to see how the bulbs are controlled. There are two options which various people have also started to do. I found that Henryk Plötz had already discovered the protocol and i used his code to transmit the signals using an arduino.

I actually used a nRF24L01+ to transmit to the PL1167 on the milight bulb and also receive from the milight remote control. The second method uses a PL1167 to do the transmission as demonstrated by Authometion.  Woodster has published the code on how to do that. So in order to do a brain transplant of the milight bulb it would be needed to receive the RF 2.4GHz signal and translate that to the status of the RGB leds. Below you can see my attempts on an arduino with my own nRF24L01 prototype board and the nRF24L01 fitted.

 

Milight bulb oldnRF24L01 proto
Above picture is the RGBW 6W lightbulb using unencrypted milight protocol.

Milight protocol for the 6W RGBW bulb is as follows:

B0 F2 EA 6D B0 02 F0
|                |    |   |   |
|                |    |   |      sequence number
|                |    |    CMD (0x1-0xF are buttons, LSB of upper nibble is long press, 0x0 see below
|                |    brightness (0x90-0x00 and 0xB0-0xF8) increments of 8
|                  color (0x00-0xFF)
ID of Remote (upper nibble first byte plus bytes 2 & 3)
Lower nibble is disco mode (see table)

CMD byte = 0x00 means one of the touch sliders was used and released.
The remote streams every intermediate value immediately with no resends, but then adds the customary amount of resends –with CMD 0x00 after you let go. In this case the sequence number can increment more than 1. The least significant bit of the upper nibble in the CMD byte indicates a long press on the corresponding button:
0x11 is “all: white”, 0x12 is “all: night mode”, 0x13 is “group 1: white”, and so on.
This applies to all buttons even to the disco mode buttons (but doesn’t seem to trigger any functionality in the bulb).
The value of the sliders are as follows:
Color. Goes from 0x00 to 0xFF.
0x00 is  9 o’clock (purple), 0x40 is  6 o’clock (yellow),  0x80 is  3 o’clock (green), 0xC0 is 12 o’clock (blue). See color wheen below.
Brightness: Goes from 0x90 at the leftmost end to 0x20 in the middle, after getting to 0x00 it jumps to 0xF8 and then reduces to 0xB0 on the rightmost end. The increments are in 0x08.
When pressing the disco button the modes are cycled and the current disco mode is reflected in the lower nibble of the first byte. This cycles from 0xB0 to 0xB8 and then cycles back to 0xB0. This disco mode is maintained when pressing other buttons, but reset on touching the color wheel.

Below on the left you can see the button numbers and on the right the so called “disco” mode and what this means.
0x0E means the brightness slider and 0x0F is the color slider.

Available Commands
0x01, // All ON                                       0x0B, // Disco Speed Increase      milight modes
0x02, // All OFF                                      0x0C, // Disco Speed Decrease
0x03, // Group 1 ON                             0x0D, // Disco Mode
0x04, // Group 1 OFF                            0x11, // Set Color White – All Groups
0x05, // Group 2 ON                             0x13, // Set Color White – Group 1
0x06, // Group 2 OFF                            0x15, // Set Color White – Group 2
0x07, // Group 3 ON                             0x17, // Set Color White – Group 3
0x08, // Group 3 OFF                            0x19, // Set Color White – Group 4
0x09, // Group 4 ON
0x0A, // Group 4 OFF
0x12, // Night Mode – All Groups
0x14, // Night Mode – Group 1             0x16, // Night Mode – Group 2
0x18, // Night Mode – Group 3             0x1A  // Night Mode – Group 4

The software for receiving and transmitting to the milight bulb is here.
[spoiler effect=”blind” show=”show  nRF24L01 decoding program” hide=”hide”]

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h> //http://tmrh20.github.io/RF24/
#include "PL1167_nRF24.h"
#include "MiLightRadio.h"

// define connection pins for nRF24L01 shield on www.arduino-projects4u.com
#define CE_PIN 9
#define CSN_PIN 10


RF24 radio(CE_PIN, CSN_PIN);
PL1167_nRF24 prf(radio);
MiLightRadio mlr(prf);

void setup()
{
  Serial.begin(115200);
  Serial.println();
  delay(1000);
  Serial.println("# OpenMiLight Receiver/Transmitter starting");
  mlr.begin();
}


static int dupesPrinted = 0;
static bool receiving = true; //set receiving true or false
static bool escaped = false;
static uint8_t outgoingPacket[9];
static uint8_t outgoingPacketPos = 0;
static uint8_t nibble;
uint8_t crc;
static enum {
  IDLE,
  HAVE_NIBBLE,
  COMPLETE,
} state;

void loop()
{
  if (receiving) {
    if (mlr.available()) {
      uint8_t packet[9];
      size_t packet_length = sizeof(packet);
      Serial.println();//     printf("n");
      Serial.print("<-- ");
      if (packet_length<0x10) Serial.print("0");
      Serial.print(packet_length,HEX);// printf("%02X ", packet_length);
      Serial.print(" ");     
      mlr.read(packet, packet_length);

      for (int i = 0; i < packet_length; i++) {
        if (packet[i]<0x10) Serial.print("0");
        Serial.print(packet[i],HEX);// printf("%02X ", packet[i]);
        Serial.print(" ");
      }
      if ((prf.crcprint>>8)<0x10) Serial.print("0");    
      Serial.print(prf.crcprint>>8,HEX);
      if ((prf.crcprint&0xFF)<0x10) Serial.print("0");
      Serial.print(prf.crcprint&0xFF,HEX);
      Serial.print(" ");    
    }
    int dupesReceived = mlr.dupesReceived();
    for (; dupesPrinted < dupesReceived; dupesPrinted++) {
    Serial.print(".");//      printf(".");
    }
  }

  while (Serial.available()) {
    char inChar = (char)Serial.read();
    uint8_t val = 0;
    bool have_val = true;

    if (inChar >= '0' && inChar <= '9') {
      val = inChar - '0';
    } else if (inChar >= 'a' && inChar <= 'f') {
      val = inChar - 'a' + 10;
    } else if (inChar >= 'A' && inChar <= 'F') {
      val = inChar - 'A' + 10;
    } else {
      have_val = false;
    }

    if (!escaped) {
      if (have_val) {
        switch (state) {
          case IDLE:
            nibble = val;
            state = HAVE_NIBBLE;
            break;
          case HAVE_NIBBLE:
            if (outgoingPacketPos < sizeof(outgoingPacket)) {
              outgoingPacket[outgoingPacketPos++] = (nibble << 4) | (val);
            } else {
              Serial.println("# Error: outgoing packet buffer full/packet too long");
            }
            if (outgoingPacketPos >= sizeof(outgoingPacket)) {
              state = COMPLETE;
              //Serial.println("nCOMPLETE");
            } else {
              state = IDLE;
            }
            break;
          case COMPLETE:
            Serial.println("# Error: outgoing packet complete. Press enter to send.");
            break;
        }
      } else {
        switch (inChar) {
          case ' ':
          case 'n':
          case 'r':
          case '.':
            if (state == COMPLETE) {
              mlr.write(outgoingPacket, sizeof(outgoingPacket));
              Serial.print("n--> ");
              if (sizeof(outgoingPacket)<0x10) Serial.print("0");              
              Serial.print(sizeof(outgoingPacket),HEX);
              Serial.print(" ");
              for (int i=0; i<sizeof(outgoingPacket);i++){
              if (outgoingPacket[i]<0x10) Serial.print("0");
              Serial.print(outgoingPacket[i],HEX);
              Serial.print(" ");
              }
              if ((prf.crcprint>>8)<0x10) Serial.print("0");    
              Serial.print(prf.crcprint>>8,HEX);
              if ((prf.crcprint&0xFF)<0x10) Serial.print("0");
              Serial.print(prf.crcprint&0xFF,HEX);
              Serial.print(" ");
            }
            if(inChar != ' ') {
              outgoingPacketPos = 0;

              state = IDLE;
            }
            if (inChar == '.') {
              mlr.resend();
              Serial.print(".");
              delay(1);
            }
            break;
          case 'x':
            Serial.println("# Escaped to extended commands: r - Toggle receiver; Press enter to return to normal mode.");
            escaped = true;
            break;
        }
      }
    } else {
      switch (inChar) {
        case 'n':
        case 'r':
          outgoingPacketPos = 0;
          state = IDLE;
          escaped = false;
          break;
        case 'r':
          receiving = !receiving;
          if (receiving) {
            Serial.println("# Now receiving");
          } else {
            Serial.println("# Now not receiving");
          }
          break;
      }
    }
  }
}


[/spoiler]
Here you can download the whole thing including the attachments. Below you can see the output of the program showing both incoming and outgoing transmissions. This is indicated by the leading arrow. To the left <– is incoming and to the right –> outgoing.


RGBW remote codes2milight CWWW remote codesmilightcolorwheel

Sending routine:
Type in 7 hexcodes back to back without space.
Eg B05465CCC001E2……..
The letters A-F can be upper or lower case.
Type a number of “.” characters at end this will repeat the message and make the transmission reliable
X gives you extended commands:
You can toggle the receiver with r
You can repeat the last transmission with “.”
In the PL1167_nRF24.cpp file you can uncomment the //#define DEBUG_PRINTF to get additional information about the package and the conversion to human readible bytes.
The Milight bulbs uses the PL1167 chip for transmission. In the table below you can see how the transmission of a certain 7 byte message is padded with preamble, sync word CRC and trailer to get a certain sequence of bits on air. To mimic that using the nRF24L01 chip you have to manipulate the transmission quite a bit because the different bits are processed in a different way and put on air differently. Below you can see how this can be done to get the same sequence of bits on air. Similarly decoding the packets also needs this manipulation to recover the actual message.
Below you can see how the nRF24L01 is manipulated to transmit the message conform the milight protocol.

nRF24L01 over air

The syncword is different for the three different remotes i tested as was the transmitter channels.
Remote type          Sync word         Transmission channels
RGBW                  0x147A 0x258B           9, 40, 71
CW WW               0x0a0A 0x55AA         4, 39, 74

Observations from light bulb operations.
RemoteID = (2 bytes) This is stored into the bulb’s EEPROM during remote-bulb sync at power on,
max of 4 per bulb, syncing a 5th RemoteID drops the first one that is stored in that bulb.
When clearing the bulb it removes all 4 remote ids from the bulb
Checksum = See checksum calc function code below. 2 Bytes
Uses Channel 9 2411MHz
Repeat again using Channel 40 2442MHz
Repeat again using Channel 71 2473MHz
Repeat again 5 to 40 times to ensure the bulbs receive the command.

The CRC routine can be simulated as follows:

[spoiler effect=”blind” show=”show CRC code” hide=”hide”]

uint16 calc_crc(uint8 data[], data_length = 0x08) {
uint16 state = 0;
for (size i = 0; i < data_length; i++) {
uint8 byte = data[i];
for (int j = 0; j < 8; j++) {
if ((byte ^ state) & 0x01) {
state = (state >> 1) ^ 0x8408;
} else {
state = state >> 1;
}
byte = byte >> 1;
}
}  return state;
}

[/spoiler]

Controller PCB for bulb control

So next step now that i could receive the 2.4GHz signal from the transmitter was to design a circuit that could replace the STM8S003F3P6 and PL1167 on the original controller board on the milight bulb. It would be simpler to program the STM8S003F3P6 controller with a new software but i do not know how to do that. If anyone does please let me know. Below you can find my design for this circuit.  Its a very small print of 25 x 23 mm so that it replaces the current microprocessor and RF receiver. You can see the panel PCB that i had made which reduces the cost of making the PCB significantly. It takes about 10-15mins to solder all the components to the PCB and then you are good to go.

RGBLED6pinv021brdfrontRGBLED6pinv021brdbackmilight panel

RGBLED6pinv021sch

I have now tested this circuit and it works as advertised. The next steps in the project are to write the software to control the milight bulb from the remote using the above. I will then be able to modify the functionality of the bulb using first the remote. At this moment i am cleaning up the code and trying to make the RGBW and CW WW remotes work on the same Milight bulb.

RGBLED6pinpcbfillv021RGBLED6pinpcbfillbackv021
nRF24L01 pinout

Once the circuit is built you need to program the bootloader into it. This is done by connecting it to an arduino as per the diagram below. You can see that it uses the arduino X3 connector and a software which you can download from here. The link to the original software has disappeared so this is a local copy. The function of the X3 interface and how to use it is described here. This is the reason the RESET pin has the prominent position on the board.

RGBLED6pinv021arduinobitbangRGB board modified

 

I use the optiboot bootloader which you can download. Please use the appropriate version. I used the  optiboot_atmega328.hex file as that is the processor and clock speed i used 16MHz. The fuse settings are as follows for an Atmega328.  hfuse = DA  lfuse = FF  efuse = 05. If you use a different processor you need different fuse settings. Once the bootloader is programmed you can use a normal FTDI cable connected as shown (Please observe colors)  below and program the device using the arduino IDE.

FDTI connectionMilight brain existingmilight brain2

It is not possible to use CS or MOSI together with PWM. To solve this problem there are two solutions. Use a non SPI pin such as the  PD4 together with softPWM. With softPWM you can get PWM on any pin without using the pins required for SPI. Another solution is to use software SPI. This way you can use pins 10 and 13 intended for SPI for PWM operation and use any other pin for software SPI. I opted for using both software solutions. (softPWM and softSPI)  The software SPI is infact included in the nRF24 software. You need to install the library from here https://github.com/greiman/DigitalIO Then you simply enable the #define SOFTSPI in the RF24_config.h file.

Replacing the Milight LED PCB

Below is the schematic for the milight LED PCB replacing the original. The original 6W RGBW Milight bulb has LED’s which depending on the version of the PCB are 5730 or 2835. In both cases there are 10 pcs. In addition there are 4 pcs of RGB led’s which provide the colour on the original milight print.  The white LED’s on the milight PCB can be WW or CW but not both.
This new designed PCB has a total of 20 pcs of LED for regular 5730 or 5630 LED’s. It is constructed in such a way that 10 pcs of cold white and 10 pcs of warm white can be controlled independantly. You could also choose to place all 20 pcs with the same white colour for more brightness. There is also a jumper present so that you can control all the LED’s using only 4 channels from the controller print. There are no vias and so i made the backside just a copper layer which should conduct the heat very well to the aluminium heat sink. You attach the PCB using the two small holes which screw into the heat sink. To drive this you need a total of 5 channels from the driver PCB which is provided for. It means the PCB has the capability to control the white channels independantly changing the white tone.
In addition there are 4 regular RGB LED chips which provide the colour. These are controlled by three independant channels from the controller board.

Milight large Milight small

Two versions of the original Milight LED PCB with 5630 and 2835 LED sizes. The clone LED PCB is designed for the larger 5630 LED’s.

3

 

4LED PCB


Donor Milight lamp

There are two methods for making a DIY milight compatible lamp.
Firstly you can use a donor Milight bulb.  You need to open this up and desolder the print with the processor and PL1167. You then solder the controller PCB in its place.
this way you use the Milight casing and also the original power supply. It is also possible to replace the LED PCB in addition which gives the advantage of more LED’s and the ability to use both WW and CW led’s.

Cloning Milight from scratch

The second method you do the whole thing from scratch.  You buy a bulb which comes with a base and lid. Taking off the lid leaves you with the base. The 220V parts are connected to wires. You can solder these to the power supply. This regulates 12V for use by the controller PCB above. A diode drops the voltage to 11.3V and then the AMS1117 regulator makes 5V out of it on which the microprocessor works. The 12V is passed through to the LED circuit so that this is the voltage on which that works. There is also a aluminium disc which serves as a heat sink. You need to be very carefull to mount the power supply as the inside of the housing is also aluminium and this of course conducts electricity and could shock you. With the milight version the power supply is already mounted in the base. As the cost of the bulb is below 8€ including delivery its hardly worth making the effort for the total DIY solution. To give you an idea I paid 1.19€ for the power supply and 1.70€ for the base, lid and heat sink but with an additional 2.56€ shipment per piece. I think the latter can be done more cheaply if you look around. Then you need some epoxy resin to glue the PCB in place which costs around 1€. So in all its also around 6.50€ vs 7.98€ for a complete Milight cone. So no brainer use a donar lamp.

Software

Still work in progress i’m afraid. Soon.
Below is a small piece of software that is able to distinguish between a RGBW remote and a CW WW remote.
I want to use this to automatically react to both types of remotes. The above software is working with RGBW only.
If you want to change the above software to work with the CW WW remote you need to change  retval = _pl1167.setSyncword(0x147A, 0x258B); on line 42 of MiLightRadio.cpp to retval = _pl1167.setSyncword(0x050A, 0x55AA); and in addition change the channels static const uint8_t CHANNELS[] = {9, 40, 71}; to 4, 39 and 74.
Of course the meaning of the buttons are different so interpretation also needs to be adjusted.

[spoiler effect=”blind” show=”show automatic switch CW/WW RGBW” hide=”hide”]

#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
#include "printf.h"

// Set up nRF24L01 radio on SPI bus plus pins 9 & 10
// Milight usage of RF channels and syncword
//         RF Channels      Syncword
// RGBW     40 71 9       14 7A  25 8B
// CW/WW    39 74 4       05 0A  55 AA
// RGBWW/CW 39 70 8       72 36  18 09
// Channels 39 and 40 very close together so take channel 4 and 9 to differentiate between remotes
// Counting starts with 0 on channel but 1 with setChannel instruction: means add one
RF24 radio(9,10);




void setup(void)
{
  Serial.begin(115200);
  printf_begin();
  Serial.println(F("nrRF24/examples/scanner/"));

  radio.begin();
  radio.setAutoAck(false);

  // Get into standby mode
  radio.startListening();
  radio.stopListening();

}

void loop()
{
      int setdelay = 225;
      radio.setChannel(5);      
      radio.startListening(); 
      delayMicroseconds(setdelay);
      radio.stopListening();        
      Serial.print(radio.testCarrier() ? "CW / WW remote n" : ""); 
      radio.setChannel(10);  
      radio.startListening(); 
      delayMicroseconds(setdelay);
      radio.stopListening();      
      Serial.print(radio.testCarrier() ? "RGBW remote n" : ""); 
             
}


[/spoiler]