Skip to content

Commit

Permalink
Merge pull request #22 from sidoh/new_protocol
Browse files Browse the repository at this point in the history
v1.0.0 release
  • Loading branch information
sidoh committed Mar 26, 2017
2 parents 2165443 + 9e76eeb commit 48355c2
Show file tree
Hide file tree
Showing 38 changed files with 1,903 additions and 660 deletions.
47 changes: 27 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,39 @@ This module is an SPI device. [This guide](https://www.mysensors.org/build/esp82

#### Setting up the ESP

1. Build from source. I use [PlatformIO](http://platformio.org/), but it's probably not hard to build this in the Arduino IDE.
2. Flash an ESP8266 with the firmware.
3. Connect to the "ESP XXXX" WiFi network to configure network settings. Alternatively you can update `main.cpp` to connect to your network directly.
You'll need to flash the firmware and a SPIFFS image. It's really easy to do this with [PlatformIO](http://platformio.org/):

#### Installing the Web UI
```
export ESP_BOARD=nodemcuv2
platformio run -u $ESP_BOARD --target upload
platformio run -u $ESP_BOARD --target uploadfs
```

The HTTP endpoints (shown below) will be fully functional at this point, but the firmware doesn't ship with a web UI (I didn't want to maintain a website in Arduino Strings).
Of course make sure to substitute `nodemcuv2` with the board that you're using.

If you want the UI, upload it to the `/web` endpoint. curl command:
#### Configure WiFi

```
$ curl -X POST -F 'image=@web/index.html' <ip of ESP>/web
success%
```
This project uses [WiFiManager](https://github.com/tzapu/WiFiManager) to avoid the need to hardcode AP credentials in the firmware.

You should now be able to navigate to `http://<ip of ESP>`. It should look like this:
When the ESP powers on, you should be able to see a network named "ESPXXXXX", with XXXXX being an identifier for your ESP. Connect to this AP and a window should pop up prompting you to enter WiFi credentials.

#### Use it!

The HTTP endpoints (shown below) will be fully functional at this point. You should also be able to navigate to `http://<ip_of_esp>`. The UI should look like this:

![Web UI](http://imgur.com/XNNigvL.png)

## REST endpoints

1. `GET /`. Opens web UI. You'll need to upload it first.
2. `GET /settings`. Gets current settings as JSON.
3. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
4. `GET /gateway_traffic`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is.
5. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. Since protocols for RGBW/CCT are different, specify one of `rgbw` or `cct` as `:device_type. Accepts a JSON blob.
6. `PUT /gateways/:device_id/:device_type`. A few commands have support for being sent to all groups. You can send those here.
7. `POST /firmware`. OTA firmware update.
8. `POST /web`. Update web UI.
1. `GET /settings`. Gets current settings as JSON.
1. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
1. `GET /radio_configs`. Get a list of supported radio configs (aka `device_type`s).
1. `GET /gateway_traffic/:device_type`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is. Since protocols for RGBW/CCT are different, specify one of `rgbw`, `cct`, or `rgb_cct` as `:device_type. Accepts a JSON blob.
1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`.
1. `PUT /gateways/:device_id/:device_type`. A few commands have support for being sent to all groups. You can send those here.
1. `POST /firmware`. OTA firmware update.
1. `POST /web`. Update web UI.

#### Bulb commands

Expand All @@ -63,8 +67,9 @@ Route (5) supports these commands:
1. `status`. Toggles on/off. Can be "on", "off", "true", or "false".
2. `hue`. (RGBW only) This is the only way to control color with these bulbs. Should be in the range `[0, 359]`.
3. `level`. (RGBW only) Controls brightness. Should be in the range `[0, 100]`.
4. `temperature`. (CCT only) Controls white temperature. Should be in the range `[0, 10]`.
5. `command`. Sends a command to the group. Can be one of:
4. `temperature`. (CCT only) Controls white temperature. Should be in the range `[0, 100]`.
5. `saturation`. (new RGB+CCT only) Controls saturation.
6. `command`. Sends a command to the group. Can be one of:
* `set_white`. (RGBW only) Turns off RGB and enters WW/CW mode.
* `pair`. Emulates the pairing process. Send this command right as you connect an unpaired bulb and it will pair with the device ID being used.
* `unpair`. Emulates the unpairing process. Send as you connect a paired bulb to have it disassociate with the device ID being used.
Expand All @@ -91,3 +96,5 @@ true%
## UDP Gateways

You can add an arbitrary number of UDP gateways through the REST API or through the web UI. Each gateway server listens on a port and responds to the standard set of commands supported by the Milight protocol. This should allow you to use one of these with standard Milight integrations (SmartThings, Home Assistant, OpenHAB, etc.).

You can select between versions 5 and 6 of the UDP protocol (documented [here](http://www.limitlessled.com/dev/)). Version 6 has support for the newer RGB+CCT bulbs and also includes response packets, which can theoretically improve reliability. Version 5 has much smaller packets and is probably lower latency.
112 changes: 83 additions & 29 deletions web/index.html → data/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
"http_repeat_factor"
];

var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
var DEFAULT_UDP_PROTOCL_VERSION = 5;

var selectize;

var toHex = function(v) {
Expand All @@ -94,6 +97,10 @@
alert("Please enter a device ID.");
throw "Must enter device ID";
}

if (! $('#group-option').data('for').split(',').includes(mode)) {
groupId = 0;
}

return "/gateways/" + deviceId + "/" + mode + "/" + groupId;
}
Expand Down Expand Up @@ -127,7 +134,7 @@
});
};

var gatewayServerRow = function(deviceId, port) {
var gatewayServerRow = function(deviceId, port, version) {
var elmt = '<tr>';
elmt += '<td>';
elmt += '<input name="deviceIds[]" class="form-control" value="' + deviceId + '"/>';
Expand All @@ -136,6 +143,20 @@
elmt += '<input name="ports[]" class="form-control" value="' + port + '"/>';;
elmt += '</td>';
elmt += '<td>';
elmt += '<div class="btn-group" data-toggle="buttons">';

for (var i = 0; i < UDP_PROTOCOL_VERSIONS.length; i++) {
var val = UDP_PROTOCOL_VERSIONS[i]
, selected = (version == val || (val == DEFAULT_UDP_PROTOCL_VERSION && !UDP_PROTOCOL_VERSIONS.includes(version)));

elmt += '<label class="btn btn-secondary' + (selected ? ' active' : '') + '">';
elmt += '<input type="radio" name="versions[]" autocomplete="off" data-value="' + val + '" '
+ (selected ? 'checked' : '') +'> ' + val;
elmt += '</label>';
}

elmt += '</div></td>';
elmt += '<td>';
elmt += '<button class="btn btn-danger remove-gateway-server">';
elmt += '<i class="glyphicon glyphicon-remove"></i>';
elmt += '</button>';
Expand Down Expand Up @@ -165,7 +186,7 @@
var gatewayForm = $('#gateway-server-configs').html('');
if (val.gateway_configs) {
val.gateway_configs.forEach(function(v) {
gatewayForm.append(gatewayServerRow(toHex(v[0]), v[1]));
gatewayForm.append(gatewayServerRow(toHex(v[0]), v[1], v[2]));
});
}
});
Expand Down Expand Up @@ -200,11 +221,15 @@
return val;
}
});

var versions = $('.active input[name="versions[]"]', form).map(function(i, v) {
return $(v).data('value');
});

if (!errors) {
var data = [];
for (var i = 0; i < deviceIds.length; i++) {
data[i] = [deviceIds[i], ports[i], 0];
data[i] = [deviceIds[i], ports[i], versions[i]];
}
$.ajax(
'/settings',
Expand All @@ -230,10 +255,10 @@
var currentMode = getCurrentMode();

$('.mode-option').map(function() {
if ($(this).data('for') != currentMode) {
$(this).hide();
} else {
if ($(this).data('for').split(',').includes(currentMode)) {
$(this).show();
} else {
$(this).hide();
}
});
};
Expand Down Expand Up @@ -409,24 +434,26 @@ <h1>
</div>

<div class="col-sm-3">
<label for="groupId">Group</label>
<div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct">
<label for="groupId">Group</label>

<div class="btn-group" id="groupId" data-toggle="buttons">
<label class="btn btn-secondary active">
<input type="radio" name="options" autocomplete="off" data-value="1" checked> 1
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="2"> 2
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="3"> 3
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="4"> 4
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="0"> All
</label>
<div class="btn-group" id="groupId" data-toggle="buttons">
<label class="btn btn-secondary active">
<input type="radio" name="options" autocomplete="off" data-value="1" checked> 1
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="2"> 2
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="3"> 3
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="4"> 4
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="0"> All
</label>
</div>
</div>
</div>

Expand All @@ -440,12 +467,18 @@ <h1>
<label class="btn btn-secondary">
<input type="radio" name="mode" autocomplete="off" data-value="cct"> CCT
</label>
<label class="btn btn-secondary">
<input type="radio" name="mode" autocomplete="off" data-value="rgb_cct"> RGB+CCT
</label>
<label class="btn btn-secondary">
<input type="radio" name="mode" autocomplete="off" data-value="rgb"> RGB
</label>
</div>
</div>
</div>

<div class="row"><div class="col-sm-12">
<div class="mode-option" data-for="rgbw">
<div class="mode-option" data-for="rgbw,rgb_cct,rgb">
<div class="row">
<div class="col-sm-12">
<h5>Hue</h5>
Expand All @@ -462,8 +495,25 @@ <h5>Hue</h5>
</div>
</div>
</div></div>

<div class="mode-option" data-for="rgb_cct">
<div class="row">
<div class="col-sm-12">
<h5>Saturation</h5>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<input class="slider raw-update" name="saturation"
data-slider-min="0"
data-slider-max="100"
data-slider-value="100"
/>
</div>
</div>
</div>

<div class="mode-option" data-for="cct">
<div class="mode-option" data-for="cct,rgb_cct">
<div class="row">
<div class="col-sm-12">
<h5>Color Temperature</h5>
Expand All @@ -473,8 +523,8 @@ <h5>Color Temperature</h5>
<div class="col-sm-6">
<input class="slider raw-update" name="temperature"
data-slider-min="0"
data-slider-max="10"
data-slider-value="10"
data-slider-max="100"
data-slider-value="100"
/>
</div>
</div>
Expand Down Expand Up @@ -508,7 +558,7 @@ <h5>Commands</h5>
<li>
<input type="checkbox" name="status" class="raw-update" data-toggle="toggle" checked/>
</li>
<div class="mode-option inline" data-for="rgbw">
<div class="mode-option inline" data-for="rgbw,rgb_cct">
<li>
<button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
</li>
Expand Down Expand Up @@ -550,6 +600,7 @@ <h1>Gateway Servers</h1>
<tr>
<th>Device ID</th>
<th>UDP Port</th>
<th>Protocol Version</th>
</tr>
</thead>
<tbody id="gateway-server-configs">
Expand Down Expand Up @@ -598,7 +649,10 @@ <h1>Sniff Traffic</h1>
<input type="radio" name="options" autocomplete="off" data-value="cct"> CCT
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="rgbw_cct"> RGBW+CCT
<input type="radio" name="options" autocomplete="off" data-value="rgb_cct"> RGB+CCT
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="rgb"> RGB
</label>
</div>

Expand Down
Binary file modified dist/firmware-d1-mini.bin
Binary file not shown.
Binary file modified dist/firmware-esp07.bin
Binary file not shown.
Binary file modified dist/firmware-esp12.bin
Binary file not shown.
Binary file modified dist/firmware-nodemcuv2.bin
Binary file not shown.
25 changes: 22 additions & 3 deletions lib/Helpers/IntParsing.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
#include <Arduino.h>

template <typename T>
const T strToHex(const String& s) {
const T strToHex(const char* s, size_t length) {
T value = 0;
T base = 1;

for (int i = s.length() - 1; i >= 0; i--) {
const char c = s.charAt(i);
for (int i = length-1; i >= 0; i--) {
const char c = s[i];

if (c >= '0' && c <= '9') {
value += ((c - '0') * base);
Expand All @@ -27,6 +27,11 @@ const T strToHex(const String& s) {
return value;
}

template <typename T>
const T strToHex(const String& s) {
return strToHex<T>(s.c_str(), s.length());
}

template <typename T>
const T parseInt(const String& s) {
if (s.startsWith("0x")) {
Expand All @@ -36,4 +41,18 @@ const T parseInt(const String& s) {
}
}

template <typename T>
void hexStrToBytes(const char* s, const size_t sLen, T* buffer, size_t maxLen) {
int idx = 0;

for (int i = 0; i < sLen && idx < maxLen; ) {
buffer[idx++] = strToHex<T>(s+i, 2);
i+= 2;

while (i < (sLen - 1) && s[i] == ' ') {
i++;
}
}
}

#endif
Loading

0 comments on commit 48355c2

Please sign in to comment.