浏览代码

initial commit: mostly functional client

Chris Mullins 8 年之前
当前提交
2dda5d4bc3

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.pioenvs
+.piolibdeps
+.clang_complete
+.gcc-flags.json

+ 65 - 0
.travis.yml

@@ -0,0 +1,65 @@
+# Continuous Integration (CI) is the practice, in software
+# engineering, of merging all developer working copies with a shared mainline
+# several times a day < http://docs.platformio.org/page/ci/index.html >
+#
+# Documentation:
+#
+# * Travis CI Embedded Builds with PlatformIO
+#   < https://docs.travis-ci.com/user/integration/platformio/ >
+#
+# * PlatformIO integration with Travis CI
+#   < http://docs.platformio.org/page/ci/travis.html >
+#
+# * User Guide for `platformio ci` command
+#   < http://docs.platformio.org/page/userguide/cmd_ci.html >
+#
+#
+# Please choice one of the following templates (proposed below) and uncomment
+# it (remove "# " before each line) or use own configuration according to the
+# Travis CI documentation (see above).
+#
+
+
+#
+# Template #1: General project. Test it using existing `platformio.ini`.
+#
+
+# language: python
+# python:
+#     - "2.7"
+#
+# sudo: false
+# cache:
+#     directories:
+#         - "~/.platformio"
+#
+# install:
+#     - pip install -U platformio
+#
+# script:
+#     - platformio run
+
+
+#
+# Template #2: The project is intended to by used as a library with examples
+#
+
+# language: python
+# python:
+#     - "2.7"
+#
+# sudo: false
+# cache:
+#     directories:
+#         - "~/.platformio"
+#
+# env:
+#     - PLATFORMIO_CI_SRC=path/to/test/file.c
+#     - PLATFORMIO_CI_SRC=examples/file.ino
+#     - PLATFORMIO_CI_SRC=path/to/test/directory
+#
+# install:
+#     - pip install -U platformio
+#
+# script:
+#     - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N

+ 74 - 0
lib/MiLight/MiLightClient.cpp

@@ -0,0 +1,74 @@
+#include <MiLightClient.h>
+
+uint8_t MiLightClient::nextSequenceNum() {
+  return sequenceNum++;
+}
+
+bool MiLightClient::available() {
+  return radio.available();
+}
+
+void MiLightClient::read(MiLightPacket& packet) {
+  uint8_t *packetBytes = reinterpret_cast<uint8_t*>(&packet);
+  size_t length = sizeof(packet);
+  radio.read(packetBytes, length);
+}
+
+void MiLightClient::write(MiLightPacket& packet, const unsigned int resendCount) {
+  uint8_t *packetBytes = reinterpret_cast<uint8_t*>(&packet);
+  
+  for (int i = 0; i < resendCount; i++) {
+    radio.write(packetBytes, sizeof(packet));
+  }
+  Serial.println();
+}
+
+
+void MiLightClient::write(
+  const uint16_t deviceId,
+  const uint16_t color,
+  const uint8_t brightness,
+  const uint8_t groupId,
+  const MiLightButton button) {
+    
+  // Expect an input value in [0, 255]. Map it down to [0, 25].
+  const uint8_t adjustedBrightness = round(brightness * (25 / 255.0));
+  
+  // The actual protocol uses a bizarre range where min is 16, max is 23:
+  // [16, 15, ..., 0, 31, ..., 23]
+  const uint8_t packetBrightnessValue = (
+    ((31 - adjustedBrightness) + 17) % 32
+  );
+  
+  // Map color as a Hue value in [0, 359] to [0, 255]. The protocol also has
+  // 0 being roughly magenta (#FF00FF)
+  const int16_t remappedColor = (color + 40) % 360;
+  const uint8_t adjustedColor = round(remappedColor * (255 / 359.0));
+  
+  MiLightPacket packet;
+  packet.deviceType = MiLightDeviceType::RGBW;
+  packet.deviceId = deviceId;
+  packet.color = adjustedColor;
+  packet.brightnessGroupId = (packetBrightnessValue << 3) | groupId;
+  packet.button = button;
+  packet.sequenceNum = nextSequenceNum();
+  
+  write(packet);
+}
+    
+void MiLightClient::updateColor(const uint16_t deviceId, const uint8_t groupId, const uint16_t hue) {
+  write(deviceId, hue, 0, groupId, MiLightButton::COLOR);
+}
+
+void MiLightClient::updateBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness) {
+  write(deviceId, 0, brightness, groupId, MiLightButton::BRIGHTNESS);
+}
+
+void MiLightClient::updateStatus(const uint16_t deviceId, const uint8_t groupId, MiLightStatus status) {
+  uint8_t button = MiLightButton::GROUP_1_ON + ((groupId - 1)*2) + status;
+  write(deviceId, 0, 0, 0, static_cast<MiLightButton>(button));
+}
+
+void MiLightClient::updateColorWhite(const uint16_t deviceId, const uint8_t groupId) {
+  write(deviceId, 0, 0, groupId, MiLightButton::COLOR_WHITE);
+}

+ 74 - 0
lib/MiLight/MiLightClient.h

@@ -0,0 +1,74 @@
+#include <Arduino.h>
+#include <MiLightRadio.h>
+
+#ifndef _MILIGHTCLIENT_H
+#define _MILIGHTCLIENT_H
+
+enum MiLightDeviceType {
+  WHITE = 0xB0,
+  RGBW = 0xB8
+};
+
+enum MiLightButton {
+  ALL_ON      = 0x01,
+  ALL_OFF     = 0x02,
+  GROUP_1_ON  = 0x03,
+  GROUP_1_OFF = 0x04,
+  GROUP_2_ON  = 0x05,
+  GROUP_2_OFF = 0x06,
+  GROUP_3_ON  = 0x07,
+  GROUP_3_OFF = 0x08,
+  GROUP_4_ON  = 0x09,
+  GROUP_4_OFF = 0x0A,
+  SPEED_UP    = 0x0B, 
+  SPEED_DOWN  = 0x0C, 
+  DISCO_MODE  = 0x0D,
+  BRIGHTNESS  = 0x0E,
+  COLOR       = 0x0F,
+  COLOR_WHITE = 0x11
+};
+
+enum MiLightStatus { ON = 0, OFF = 1 };
+  
+#pragma pack(push, 1)
+struct MiLightPacket {
+  uint8_t deviceType;
+  uint16_t deviceId;
+  uint8_t color;
+  uint8_t brightnessGroupId;
+  uint8_t button;
+  uint8_t sequenceNum;
+};
+#pragma pack(pop)
+
+class MiLightClient {
+  public:
+    MiLightClient(MiLightRadio& radio) :
+      radio(radio),
+      sequenceNum(0) {}
+    
+    bool available();
+    void read(MiLightPacket& packet);
+    void write(MiLightPacket& packet, const unsigned int resendCount = 50);
+    
+    void write(
+      const uint16_t deviceId,
+      const uint16_t color,
+      const uint8_t brightness,
+      const uint8_t groupId,
+      const MiLightButton button
+    );
+    
+    void updateColor(const uint16_t deviceId, const uint8_t groupId, const uint16_t hue);
+    void updateBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness);
+    void updateStatus(const uint16_t deviceId, const uint8_t groupId, MiLightStatus status);
+    void updateColorWhite(const uint16_t deviceId, const uint8_t groupId);
+    
+  private:
+    MiLightRadio& radio;
+    uint8_t sequenceNum;
+    
+    uint8_t nextSequenceNum();
+};
+
+#endif

+ 134 - 0
lib/MiLight/MiLightRadio.cpp

@@ -0,0 +1,134 @@
+/*
+ * MiLightRadio.cpp
+ *
+ *  Created on: 29 May 2015
+ *      Author: henryk
+ */
+
+#include "MiLightRadio.h"
+
+#define PACKET_ID(packet) ( ((packet[1] & 0xF0)<<24) | (packet[2]<<16) | (packet[3]<<8) | (packet[7]) )
+
+static const uint8_t CHANNELS[] = {9, 40, 71};
+#define NUM_CHANNELS (sizeof(CHANNELS)/sizeof(CHANNELS[0]))
+
+MiLightRadio::MiLightRadio(AbstractPL1167 &pl1167)
+  : _pl1167(pl1167) {
+  _waiting = false;
+}
+
+int MiLightRadio::begin()
+{
+  int retval = _pl1167.open();
+  if (retval < 0) {
+    return retval;
+  }
+
+  retval = _pl1167.setCRC(true);
+  if (retval < 0) {
+    return retval;
+  }
+
+  retval = _pl1167.setPreambleLength(3);
+  if (retval < 0) {
+    return retval;
+  }
+
+  retval = _pl1167.setTrailerLength(4);
+  if (retval < 0) {
+    return retval;
+  }
+
+  retval = _pl1167.setSyncword(0x147A, 0x258B);
+  if (retval < 0) {
+    return retval;
+  }
+
+  retval = _pl1167.setMaxPacketLength(8);
+  if (retval < 0) {
+    return retval;
+  }
+
+  available();
+
+  return 0;
+}
+
+bool MiLightRadio::available()
+{
+  if (_waiting) {
+    return true;
+  }
+
+  if (_pl1167.receive(CHANNELS[0]) > 0) {
+    size_t packet_length = sizeof(_packet);
+    if (_pl1167.readFIFO(_packet, packet_length) < 0) {
+      return false;
+    }
+    if (packet_length == 0 || packet_length != _packet[0] + 1U) {
+      return false;
+    }
+
+    uint32_t packet_id = PACKET_ID(_packet);
+    if (packet_id == _prev_packet_id) {
+      _dupes_received++;
+    } else {
+      _prev_packet_id = packet_id;
+      _waiting = true;
+    }
+  }
+
+  return _waiting;
+}
+
+int MiLightRadio::dupesReceived()
+{
+  return _dupes_received;
+}
+
+
+int MiLightRadio::read(uint8_t frame[], size_t &frame_length)
+{
+  if (!_waiting) {
+    frame_length = 0;
+    return -1;
+  }
+
+  if (frame_length > sizeof(_packet) - 1) {
+    frame_length = sizeof(_packet) - 1;
+  }
+
+  if (frame_length > _packet[0]) {
+    frame_length = _packet[0];
+  }
+
+  memcpy(frame, _packet + 1, frame_length);
+  _waiting = false;
+
+  return _packet[0];
+}
+
+int MiLightRadio::write(uint8_t frame[], size_t frame_length)
+{
+  if (frame_length > sizeof(_out_packet) - 1) {
+    return -1;
+  }
+
+  memcpy(_out_packet + 1, frame, frame_length);
+  _out_packet[0] = frame_length;
+
+  int retval = resend();
+  if (retval < 0) {
+    return retval;
+  }
+  return frame_length;
+}
+
+int MiLightRadio::resend()
+{
+  for (size_t i = 0; i < NUM_CHANNELS; i++) {
+    _pl1167.writeFIFO(_out_packet, _out_packet[0] + 1);
+    _pl1167.transmit(CHANNELS[i]);
+  }
+  return 0;
+}

+ 41 - 0
lib/MiLight/MiLightRadio.h

@@ -0,0 +1,41 @@
+/*
+ * MiLightRadio.h
+ *
+ *  Created on: 29 May 2015
+ *      Author: henryk
+ */
+
+#ifdef ARDUINO
+#include "Arduino.h"
+#else
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#endif
+
+#include "AbstractPL1167.h"
+
+#ifndef MILIGHTRADIO_H_
+#define MILIGHTRADIO_H_
+
+class MiLightRadio {
+  public:
+    MiLightRadio(AbstractPL1167 &pl1167);
+    int begin();
+    bool available();
+    int read(uint8_t frame[], size_t &frame_length);
+    int dupesReceived();
+    int write(uint8_t frame[], size_t frame_length);
+    int resend();
+  private:
+    AbstractPL1167 &_pl1167;
+    uint32_t _prev_packet_id;
+
+    uint8_t _packet[8], _out_packet[8];
+    bool _waiting;
+    int _dupes_received;
+};
+
+
+
+#endif /* MILIGHTRADIO_H_ */

+ 36 - 0
lib/PL1167_nRF24/AbstractPL1167.h

@@ -0,0 +1,36 @@
+/*
+ * AbstractPL1167.h
+ *
+ *  Created on: 29 May 2015
+ *      Author: henryk
+ */
+
+#ifdef ARDUINO
+#include "Arduino.h"
+#else
+#include <stdint.h>
+#include <stdlib.h>
+#endif
+
+#ifndef ABSTRACTPL1167_H_
+#define ABSTRACTPL1167_H_
+
+class AbstractPL1167 {
+  public:
+    virtual int open() = 0;
+
+    virtual int setPreambleLength(uint8_t preambleLength) = 0;
+    virtual int setSyncword(uint16_t syncword0, uint16_t syncword3) = 0;
+    virtual int setTrailerLength(uint8_t trailerLength) = 0;
+    virtual int setMaxPacketLength(uint8_t maxPacketLength) = 0;
+    virtual int setCRC(bool crc) = 0;
+    virtual int writeFIFO(const uint8_t data[], size_t data_length) = 0;
+    virtual int transmit(uint8_t channel) = 0;
+    virtual int receive(uint8_t channel) = 0;
+    virtual int readFIFO(uint8_t data[], size_t &data_length) = 0;
+};
+
+
+
+
+#endif /* ABSTRACTPL1167_H_ */

+ 390 - 0
lib/PL1167_nRF24/PL1167_nRF24.cpp

@@ -0,0 +1,390 @@
+/*
+ * PL1167_nRF24.cpp
+ *
+ *  Created on: 29 May 2015
+ *      Author: henryk
+ */
+
+#include "PL1167_nRF24.h"
+
+static uint16_t calc_crc(uint8_t *data, size_t data_length);
+static uint8_t reverse_bits(uint8_t data);
+static void demangle_packet(uint8_t *in, uint8_t *out) ;
+
+PL1167_nRF24::PL1167_nRF24(RF24 &radio)
+  : _radio(radio) { }
+
+static const uint8_t pipe[] = {0xd1, 0x28, 0x5e, 0x55, 0x55};
+
+int PL1167_nRF24::open()
+{
+  _radio.begin();
+  return recalc_parameters();
+}
+
+int PL1167_nRF24::recalc_parameters()
+{
+  int nrf_address_length = _preambleLength - 1 + _syncwordLength;
+  int address_overflow = 0;
+  if (nrf_address_length > 5) {
+    address_overflow = nrf_address_length - 5;
+    nrf_address_length = 5;
+  }
+  int packet_length = address_overflow + ( (_trailerLength + 7) / 8) + _maxPacketLength;
+  if (_crc) {
+    packet_length += 2;
+  }
+
+  if (packet_length > sizeof(_packet) || nrf_address_length < 3) {
+    return -1;
+  }
+
+  uint8_t preamble = 0;
+  if (_syncword0 & 0x01) {
+    preamble = 0x55;
+  } else {
+    preamble = 0xAA;
+  }
+
+  int nrf_address_pos = nrf_address_length;
+  for (int i = 0; i < _preambleLength - 1; i++) {
+    _nrf_pipe[ --nrf_address_pos ] = reverse_bits(preamble);
+  }
+
+  if (nrf_address_pos) {
+    _nrf_pipe[ --nrf_address_pos ] = reverse_bits(_syncword0 & 0xff);
+  }
+  if (nrf_address_pos) {
+    _nrf_pipe[ --nrf_address_pos ] = reverse_bits( (_syncword0 >> 8) & 0xff);
+  }
+
+  if (_syncwordLength == 4) {
+    if (nrf_address_pos) {
+      _nrf_pipe[ --nrf_address_pos ] = reverse_bits(_syncword3 & 0xff);
+    }
+    if (nrf_address_pos) {
+      _nrf_pipe[ --nrf_address_pos ] = reverse_bits( (_syncword3 >> 8) & 0xff);
+    }
+  }
+
+  _receive_length = packet_length;
+  _preamble = preamble;
+
+  _nrf_pipe_length = nrf_address_length;
+  _radio.setAddressWidth(_nrf_pipe_length);
+  _radio.openWritingPipe(_nrf_pipe);
+  _radio.openReadingPipe(1, _nrf_pipe);
+
+  _radio.setChannel(2 + _channel);
+
+
+  _radio.setPayloadSize( packet_length );
+  _radio.setAutoAck(false);
+  _radio.setPALevel(RF24_PA_MAX);
+  _radio.setDataRate(RF24_1MBPS);
+  _radio.disableCRC();
+
+  return 0;
+}
+
+
+int PL1167_nRF24::setPreambleLength(uint8_t preambleLength)
+{
+  if (preambleLength > 8) {
+    return -1;
+  }
+  _preambleLength = preambleLength;
+  return recalc_parameters();
+}
+
+
+int PL1167_nRF24::setSyncword(uint16_t syncword0, uint16_t syncword3)
+{
+  _syncwordLength = 4;
+  _syncword0 = syncword0;
+  _syncword3 = syncword3;
+  return recalc_parameters();
+}
+
+int PL1167_nRF24::setTrailerLength(uint8_t trailerLength)
+{
+  if (trailerLength < 4) {
+    return -1;
+  }
+  if (trailerLength > 18) {
+    return -1;
+  }
+  if (trailerLength & 0x01) {
+    return -1;
+  }
+  _trailerLength = trailerLength;
+  return recalc_parameters();
+}
+
+int PL1167_nRF24::setCRC(bool crc)
+{
+  _crc = crc;
+  return recalc_parameters();
+}
+
+int PL1167_nRF24::setMaxPacketLength(uint8_t maxPacketLength)
+{
+  _maxPacketLength = maxPacketLength;
+  return recalc_parameters();
+}
+
+int PL1167_nRF24::receive(uint8_t channel)
+{
+  if (channel != _channel) {
+    _channel = channel;
+    int retval = recalc_parameters();
+    if (retval < 0) {
+      return retval;
+    }
+  }
+
+  _radio.startListening();
+  if (_radio.available()) {
+    internal_receive();
+  }
+
+  if(_received) {
+    return _packet_length;
+  } else {
+    return 0;
+  }
+}
+
+int PL1167_nRF24::readFIFO(uint8_t data[], size_t &data_length)
+{
+  if (data_length > _packet_length) {
+    data_length = _packet_length;
+  }
+  memcpy(data, _packet, data_length);
+  _packet_length -= data_length;
+  if (_packet_length) {
+    memmove(_packet, _packet + data_length, _packet_length);
+  }
+  return _packet_length;
+}
+
+int PL1167_nRF24::writeFIFO(const uint8_t data[], size_t data_length)
+{
+  if (data_length > sizeof(_packet)) {
+    data_length = sizeof(_packet);
+  }
+  memcpy(_packet, data, data_length);
+  _packet_length = data_length;
+  _received = false;
+
+  return data_length;
+}
+
+int PL1167_nRF24::transmit(uint8_t channel)
+{
+  if (channel != _channel) {
+    _channel = channel;
+    int retval = recalc_parameters();
+    if (retval < 0) {
+      return retval;
+    }
+  }
+
+  _radio.stopListening();
+  uint8_t tmp[sizeof(_packet)];
+
+  uint8_t trailer = (_packet[0] & 1) ? 0x55 : 0xAA;  // NOTE: This is a guess, it might also be based upon the last
+  // syncword bit, or fixed
+  int outp = 0;
+
+  for (; outp < _receive_length; outp++) {
+    uint8_t outbyte = 0;
+
+    if (outp + 1 + _nrf_pipe_length < _preambleLength) {
+      outbyte = _preamble;
+    } else if (outp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength) {
+      int syncp = outp - _preambleLength + 1 + _nrf_pipe_length;
+      switch (syncp) {
+        case 0:
+          outbyte = _syncword0 & 0xFF;
+          break;
+        case 1:
+          outbyte = (_syncword0 >> 8) & 0xFF;
+          break;
+        case 2:
+          outbyte = _syncword3 & 0xFF;
+          break;
+        case 3:
+          outbyte = (_syncword3 >> 8) & 0xFF;
+          break;
+      }
+    } else if (outp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength + (_trailerLength / 8) ) {
+      outbyte = trailer;
+    } else {
+      break;
+    }
+
+    tmp[outp] = reverse_bits(outbyte);
+  }
+
+  int buffer_fill;
+  bool last_round = false;
+  uint16_t buffer = 0;
+  uint16_t crc;
+  if (_crc) {
+    crc = calc_crc(_packet, _packet_length);
+  }
+
+  buffer = trailer >> (8 - (_trailerLength % 8));
+  buffer_fill = _trailerLength % 8;
+  for (int inp = 0; inp < _packet_length + (_crc ? 2 : 0) + 1; inp++) {
+    if (inp < _packet_length) {
+      buffer |= _packet[inp] << buffer_fill;
+      buffer_fill += 8;
+    } else if (_crc && inp < _packet_length + 2) {
+      buffer |= ((crc >>  ( (inp - _packet_length) * 8)) & 0xff) << buffer_fill;
+      buffer_fill += 8;
+    } else {
+      last_round = true;
+    }
+
+    while (buffer_fill > (last_round ? 0 : 8)) {
+      if (outp >= sizeof(tmp)) {
+        return -1;
+      }
+      tmp[outp++] = reverse_bits(buffer & 0xff);
+      buffer >>= 8;
+      buffer_fill -= 8;
+    }
+  }
+
+  _radio.write(tmp, outp);
+  return 0;
+}
+
+
+int PL1167_nRF24::internal_receive()
+{
+  uint8_t tmp[sizeof(_packet)];
+  int outp = 0;
+
+  _radio.read(tmp, _receive_length);
+
+  // HACK HACK HACK: Reset radio
+  open();
+
+  uint8_t shift_amount = _trailerLength % 8;
+  uint16_t buffer = 0;
+
+#ifdef DEBUG_PRINTF
+  printf("Packet received: ");
+  for (int i = 0; i < _receive_length; i++) {
+    printf("%02X", reverse_bits(tmp[i]));
+  }
+  printf("\n");
+#endif
+
+  for (int inp = 0; inp < _receive_length; inp++) {
+    uint8_t inbyte = reverse_bits(tmp[inp]);
+    buffer = (buffer >> 8) | (inbyte << 8);
+
+    if (inp + 1 + _nrf_pipe_length < _preambleLength) {
+      if (inbyte != _preamble) {
+#ifdef DEBUG_PRINTF
+        printf("Preamble fail (%i: %02X)\n", inp, inbyte);
+#endif
+        return 0;
+      }
+    } else if (inp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength) {
+      int syncp = inp - _preambleLength + 1 + _nrf_pipe_length;
+      switch (syncp) {
+        case 0:
+          if (inbyte != _syncword0 & 0xFF) {
+#ifdef DEBUG_PRINTF
+            printf("Sync 0l fail (%i: %02X)\n", inp, inbyte);
+#endif
+            return 0;
+          } break;
+        case 1:
+          if (inbyte != (_syncword0 >> 8) & 0xFF) {
+#ifdef DEBUG_PRINTF
+            printf("Sync 0h fail (%i: %02X)\n", inp, inbyte);
+#endif
+            return 0;
+          } break;
+        case 2:
+          if ((_syncwordLength == 4) && (inbyte != _syncword3 & 0xFF)) {
+#ifdef DEBUG_PRINTF
+            printf("Sync 3l fail (%i: %02X)\n", inp, inbyte);
+#endif
+            return 0;
+          } break;
+        case 3:
+          if ((_syncwordLength == 4) && (inbyte != (_syncword3 >> 8) & 0xFF)) {
+#ifdef DEBUG_PRINTF
+            printf("Sync 3h fail (%i: %02X)\n", inp, inbyte);
+#endif
+            return 0;
+          } break;
+      }
+    } else if (inp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength + ((_trailerLength + 7) / 8) ) {
+
+    } else {
+      tmp[outp++] = buffer >> shift_amount;
+    }
+  }
+
+
+#ifdef DEBUG_PRINTF
+  printf("Packet transformed: ");
+  for (int i = 0; i < outp; i++) {
+    printf("%02X", tmp[i]);
+  }
+  printf("\n");
+#endif
+
+
+  if (_crc) {
+    if (outp < 2) {
+      return 0;
+    }
+    uint16_t crc = calc_crc(tmp, outp - 2);
+    if ( ((crc & 0xff) != tmp[outp - 2]) || (((crc >> 8) & 0xff) != tmp[outp - 1]) ) {
+      return 0;
+    }
+    outp -= 2;
+  }
+
+  memcpy(_packet, tmp, outp);
+  _packet_length = outp;
+  _received = true;
+  return outp;
+}
+
+#define CRC_POLY 0x8408
+
+static uint16_t calc_crc(uint8_t *data, size_t data_length) {
+  uint16_t state = 0;
+  for (size_t i = 0; i < data_length; i++) {
+    uint8_t byte = data[i];
+    for (int j = 0; j < 8; j++) {
+      if ((byte ^ state) & 0x01) {
+        state = (state >> 1) ^ CRC_POLY;
+      } else {
+        state = state >> 1;
+      }
+      byte = byte >> 1;
+    }
+  }
+  return state;
+}
+
+static uint8_t reverse_bits(uint8_t data) {
+  uint8_t result = 0;
+  for (int i = 0; i < 8; i++) {
+    result <<= 1;
+    result |= data & 1;
+    data >>= 1;
+  }
+  return result;
+}

+ 59 - 0
lib/PL1167_nRF24/PL1167_nRF24.h

@@ -0,0 +1,59 @@
+/*
+ * PL1167_nRF24.h
+ *
+ *  Created on: 29 May 2015
+ *      Author: henryk
+ */
+
+#ifdef ARDUINO
+#include "Arduino.h"
+#endif
+
+#include "AbstractPL1167.h"
+#include "RF24.h"
+
+#ifndef PL1167_NRF24_H_
+#define PL1167_NRF24_H_
+
+class PL1167_nRF24 : public AbstractPL1167 {
+  public:
+    PL1167_nRF24(RF24 &radio);
+    int open();
+    int setPreambleLength(uint8_t preambleLength);
+    int setSyncword(uint16_t syncword0, uint16_t syncword3);
+    int setTrailerLength(uint8_t trailerLength);
+    int setCRC(bool crc);
+    int setMaxPacketLength(uint8_t maxPacketLength);
+    int writeFIFO(const uint8_t data[], size_t data_length);
+    int transmit(uint8_t channel);
+    int receive(uint8_t channel);
+    int readFIFO(uint8_t data[], size_t &data_length);
+
+  private:
+    RF24 &_radio;
+
+    bool _crc;
+    uint8_t _preambleLength = 1;
+    uint16_t _syncword0 = 0, _syncword3 = 0;
+    uint8_t _syncwordLength = 4;
+    uint8_t _trailerLength = 4;
+    uint8_t _maxPacketLength = 8;
+
+    uint8_t _channel = 0;
+
+    uint8_t _nrf_pipe[5];
+    uint8_t _nrf_pipe_length;
+
+    uint8_t _packet_length = 0;
+    uint8_t _receive_length = 0;
+    uint8_t _preamble = 0;
+    uint8_t _packet[32];
+    bool _received = false;
+
+    int recalc_parameters();
+    int internal_receive();
+
+};
+
+
+#endif /* PL1167_NRF24_H_ */

+ 18 - 0
platformio.ini

@@ -0,0 +1,18 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; http://docs.platformio.org/page/projectconf.html
+
+[env:nodemcuv2]
+platform = espressif8266
+board = nodemcuv2
+framework = arduino
+lib_deps = 
+  RF24
+  SPI
+  WiFiManager

+ 356 - 0
src/main.cpp

@@ -0,0 +1,356 @@
+#include <SPI.h>
+#include <nRF24L01.h>
+#include <RF24.h>
+#include <WiFiManager.h>
+
+#include <PL1167_nRF24.h>
+#include <MiLightRadio.h>
+#include <MiLightClient.h>
+
+#define CE_PIN D0 
+#define CSN_PIN D8
+
+RF24 radio(CE_PIN, CSN_PIN);
+PL1167_nRF24 prf(radio);
+MiLightRadio mlr(prf);
+MiLightClient milightClient(mlr);
+
+WiFiManager wifiManager;
+
+void setup() {
+  Serial.begin(9600);
+  wifiManager.autoConnect();
+  mlr.begin();
+}
+
+
+static int dupesPrinted = 0;
+static bool receiving = true;
+static bool escaped = false;
+static uint8_t outgoingPacket[7];
+static uint8_t outgoingPacketUDP[7];
+static uint8_t outgoingPacketPos = 0;
+static uint8_t nibble;
+static enum {
+  IDLE,
+  HAVE_NIBBLE,
+  COMPLETE,
+} state;
+
+static uint8_t reverse_bits(uint8_t data) {
+  uint8_t result = 0;
+  for (int i = 0; i < 8; i++) {
+    result <<= 1;
+    result |= data & 1;
+    data >>= 1;
+  }
+  return result;
+}
+
+void udpLoop(WiFiUDP Udp, uint8_t id1, uint8_t id2) {
+  int packetSize = Udp.parsePacket();
+  if (packetSize)
+  {
+    // read the packet into packetBufffer
+    int len = Udp.read(packetBuffer, 255);
+    if (len > 0) packetBuffer[len] = 0;
+    Serial.println();
+    Serial.print("Contents: ");
+    for (int j = 0; j < len; j++) {
+      Serial.print(packetBuffer[j], HEX);
+      Serial.print(" ");
+    }
+    Serial.println();
+
+    outgoingPacketUDP[0] = 0xB0; // B0 - White | B8 - RGBW
+    outgoingPacketUDP[1] = id1; // Remote ID
+    outgoingPacketUDP[2] = id2; // Remote ID
+
+    if (packetBuffer[0] == 0x40) {  // Color
+      outgoingPacketUDP[3] = ((uint8_t)0xFF - packetBuffer[1]) + 0xC0;
+      outgoingPacketUDP[4] = lastOnGroup; // Use last ON group
+      outgoingPacketUDP[5] = 0x0F; // Button
+
+    } else if (packetBuffer[0] == 0x4E) { // Brightness
+
+      // 2 to 1B (2-27)
+      // (0x90-0x00 and 0xF8-0xB0) increments of 8
+      // 0x90-0x00 = 1 to 18
+      // 0xB0-F8 = 19 to 27
+      /*
+       * x - 98
+       * x - 90
+       * x - 88
+       * 2 - 80*
+       * 3 - 78
+       * 4 - 70
+       * 5 - 68
+       * 6 - 60
+       * 7 - 58
+       * 8 -50
+       * 9 - 48
+       * 10 - 40
+       * 11 - 38
+       * 12 - 30
+       * 13 - 28
+       * 14 - 20
+       * 15 - 18
+       * 16 - 10
+       * 17 - 8
+       * 18 - 0
+       * 19 - F8
+       * 20 - F0
+       * 21 - E8
+       * 22 - E0
+       * 23 - D8
+       * 24 - D0
+       * 25 - C8
+       * 26 - C0
+       * 27 - B8*
+       * xx - B0
+       * xx - A8
+       */
+
+      if (packetBuffer[1] <= 18) {
+        outgoingPacketUDP[4] = (18 - packetBuffer[1]) * 0x08;
+      } else {
+        outgoingPacketUDP[4] = 0xB8 + (27 - packetBuffer[1]) * 0x08;
+      }
+      outgoingPacketUDP[4] += lastOnGroup; // add group number
+      outgoingPacketUDP[5] = 0x0E; // Button
+
+    } else if ((packetBuffer[0] & 0xF0) == 0xC0) {
+      outgoingPacketUDP[5] = packetBuffer[0] - 0xB2; // Full White
+
+    } else if (packetBuffer[0] == 0x41) {   // Button RGBW COLOR LED ALL OFF
+      outgoingPacketUDP[5] = 0x02;
+    } else if (packetBuffer[0] == 0x42) {   // Button RGBW COLOR LED ALL ON
+      outgoingPacketUDP[5] = 0x01;
+      lastOnGroup = 0;
+    } else if (packetBuffer[0] == 0x45) {   // Group 1 ON
+      outgoingPacketUDP[5] = 0x03;
+      lastOnGroup = 1;
+    } else if (packetBuffer[0] == 0x46) {   // Group 1 OFF
+      outgoingPacketUDP[5] = 0x04;
+    } else if (packetBuffer[0] == 0x47) {   // Group 2 ON
+      outgoingPacketUDP[5] = 0x05;
+      lastOnGroup = 2;
+    } else if (packetBuffer[0] == 0x48) {   // Group 2 OFF
+      outgoingPacketUDP[5] = 0x06;
+    } else if (packetBuffer[0] == 0x49) {   // Group 3 ON
+      outgoingPacketUDP[5] = 0x07;
+      lastOnGroup = 3;
+    } else if (packetBuffer[0] == 0x4A) {   // Group 3 OFF
+      outgoingPacketUDP[5] = 0x08;
+    } else if (packetBuffer[0] == 0x4B) {   // Group 4 ON
+      outgoingPacketUDP[5] = 0x09;
+      lastOnGroup = 4;
+    } else if (packetBuffer[0] == 0x4C) {   // Group 5 OFF
+      outgoingPacketUDP[5] = 0x0A;
+
+    } else {
+      Serial.println("Wooops!");
+      outgoingPacketUDP[5] = packetBuffer[0] - 0x42; // Button
+    }
+    outgoingPacketUDP[6]++; // Counter
+
+    Serial.print("Write : ");
+    for (int j = 0; j < sizeof(outgoingPacketUDP); j++) {
+      Serial.print(outgoingPacketUDP[j], HEX);
+      Serial.print(" ");
+    }
+    Serial.println();
+
+    mlr.write(outgoingPacketUDP, sizeof(outgoingPacketUDP));
+    resendCounter = 16;
+    lastMicros = micros();
+  }
+  delay(0);
+
+  if (resendCounter > 0) {
+    if (micros() - 350 > lastMicros) {
+      mlr.resend();
+      resendCounter--;
+      Serial.print(".");
+      lastMicros = micros();
+    }
+  }
+}
+
+unsigned int brightness = 0;
+uint16_t color = 360;
+
+void loop() {
+  if (receiving) {
+    if (mlr.available()) {
+      printf("\n");
+      Serial.print("Packet: ");
+      uint8_t packet[7];
+      size_t packet_length = sizeof(packet);
+      mlr.read(packet, packet_length);
+
+      for (int i = 0; i < packet_length; i++) {
+        //Serial.print(packet[i], HEX);
+        //Serial.print(" ");
+        printf("%02X ", packet[i]);
+      }
+      printf("\n");
+    }
+    // 
+    // int dupesReceived = mlr.dupesReceived();
+    // for (; dupesPrinted < dupesReceived; dupesPrinted++) {
+    //   printf(".");
+    //   Serial.print(".");
+    // }
+  }
+
+  while (Serial.available()) {
+    yield();
+    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;
+            } 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) {
+              Serial.print("Write : ");
+              for (int j = 0; j < sizeof(outgoingPacket); j++) {
+                Serial.print(outgoingPacket[j], HEX);
+                Serial.print(" ");
+              }
+              Serial.print(" - Size : ");
+              Serial.print(sizeof(outgoingPacket), DEC);
+              Serial.println();
+              mlr.write(outgoingPacket, sizeof(outgoingPacket));
+              Serial.println("Write End");
+            }
+            if (inChar != ' ') {
+              outgoingPacketPos = 0;
+              state = IDLE;
+            }
+            if (inChar == '.') {
+              mlr.resend();
+              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 'a':
+          Serial.println("Sending on packet");
+          // mlr.write(offPacket, sizeof(offPacket));
+          
+          milightClient.updateStatus(0x86CD, 2, ON);
+          
+          break;
+        case 'b':
+          milightClient.write(
+            0x86CD,
+            255,
+            ++brightness,
+            2,
+            MiLightButton::BRIGHTNESS
+          );
+          break;
+        case 'c':
+          milightClient.write(
+            0x86CD,
+            255,
+            --brightness,
+            2,
+            MiLightButton::BRIGHTNESS
+          );
+          break;
+          
+        case 'd':
+          milightClient.write(
+            0x86CD,
+            255,
+            brightness++,
+            2,
+            MiLightButton::BRIGHTNESS
+          );
+          break;
+        
+        case 'e':
+          milightClient.write(
+            0x86CD,
+            (++color%360),
+            0,
+            2,
+            MiLightButton::COLOR
+          );
+          break;
+        case 'f':
+          milightClient.write(
+            0x86CD,
+            (--color%360),
+            0,
+            2,
+            MiLightButton::COLOR
+          );
+          break;
+        case 'g':
+          milightClient.updateColorWhite(0x86CD, 2);
+          break;
+        
+        case 'r':
+          receiving = !receiving;
+          if (receiving) {
+            Serial.println("# Now receiving");
+          } else {
+            Serial.println("# Now not receiving");
+          }
+          break;
+      }
+    }
+  }
+}