First commit
This commit is contained in:
commit
9ed0e801b7
165 changed files with 21035 additions and 0 deletions
45
.build_web.py
Normal file
45
.build_web.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from shutil import copyfile
|
||||
from subprocess import check_output, CalledProcessError
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
Import("env")
|
||||
|
||||
def is_tool(name):
|
||||
cmd = "where" if platform.system() == "Windows" else "which"
|
||||
try:
|
||||
check_output([cmd, name])
|
||||
return True
|
||||
except:
|
||||
return False;
|
||||
|
||||
def build_web():
|
||||
if is_tool("npm"):
|
||||
os.chdir("web")
|
||||
print("Attempting to build webpage...")
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
print(check_output(["npm.cmd", "install", "--only=dev"]))
|
||||
print(check_output(["node_modules\\.bin\\gulp.cmd"]))
|
||||
else:
|
||||
print(check_output(["npm", "install"]))
|
||||
print(check_output(["node_modules/.bin/gulp"]))
|
||||
copyfile("build/index.html.gz.h", "../dist/index.html.gz.h")
|
||||
except OSError as e:
|
||||
print("Encountered error OSError building webpage:", e)
|
||||
if e.filename:
|
||||
print("Filename is", e.filename)
|
||||
print("WARNING: Failed to build web package. Using pre-built page.")
|
||||
except CalledProcessError as e:
|
||||
print(e.output)
|
||||
print("Encountered error CalledProcessError building webpage:", e)
|
||||
print("WARNING: Failed to build web package. Using pre-built page.")
|
||||
except Exception as e:
|
||||
print("Encountered error", type(e).__name__, "building webpage:", e)
|
||||
print("WARNING: Failed to build web package. Using pre-built page.")
|
||||
finally:
|
||||
os.chdir("..");
|
||||
|
||||
build_web()
|
31
.get_version.py
Executable file
31
.get_version.py
Executable file
|
@ -0,0 +1,31 @@
|
|||
from subprocess import check_output
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
os.chdir(dir_path)
|
||||
|
||||
# http://stackoverflow.com/questions/11210104/check-if-a-program-exists-from-a-python-script
|
||||
def is_tool(name):
|
||||
cmd = "where" if platform.system() == "Windows" else "which"
|
||||
try:
|
||||
check_output([cmd, name])
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
version = "UNKNOWN".encode()
|
||||
|
||||
if is_tool("git"):
|
||||
try:
|
||||
version = check_output(["git", "describe", "--always"]).rstrip()
|
||||
except:
|
||||
try:
|
||||
version = check_output(["git", "rev-parse", "--short", "HEAD"]).rstrip()
|
||||
except:
|
||||
pass
|
||||
pass
|
||||
|
||||
sys.stdout.write("-DMILIGHT_HUB_VERSION=%s %s" % (version.decode('utf-8'), ' '.join(sys.argv[1:])))
|
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report an issue or unexpected behavior
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
<!-- If you're reporting a bug, please provide a way to reliably reproduce the issue. -->
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Setup information
|
||||
|
||||
#### Firmware version
|
||||
<!-- e.g. 1.10.0-rc.1, etc. include all versions you've tried -->
|
||||
|
||||
#### Output of http://milight-hub.local/about
|
||||
|
||||
```json
|
||||
"... /about output. put between the ```s"
|
||||
```
|
||||
|
||||
#### Output of http://milight-hub.local/settings
|
||||
|
||||
<!-- MAKE SURE TO SENSOR ANY PASSWORDS! -->
|
||||
|
||||
```json
|
||||
"... /settings output. put between the ```s"
|
||||
```
|
||||
|
||||
### Additional context
|
10
.github/ISSUE_TEMPLATE/feature-request-or-general-question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/feature-request-or-general-question.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Feature request or general question
|
||||
about: Suggest a new idea or ask a question
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
51
.github/ISSUE_TEMPLATE/problem-with-new-setup-or-device-compatibility.md
vendored
Normal file
51
.github/ISSUE_TEMPLATE/problem-with-new-setup-or-device-compatibility.md
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: Problem with new setup or device compatibility
|
||||
about: Help troubleshooting a setup that's not working
|
||||
title: ''
|
||||
labels: setup-quesetion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
-- !!! PLEASE READ THIS FIRST !!!
|
||||
--
|
||||
-- Before opening an issue for a setup-related question, please make sure you've run through
|
||||
-- the troubleshooting guide, found here:
|
||||
--
|
||||
-- https://github.com/sidoh/esp8266_milight_hub/wiki/Troubleshooting
|
||||
--
|
||||
-- If you're still having trouble, please make sure you provide all of the requested information below!
|
||||
-->
|
||||
|
||||
### What is the model number of the device you're trying to control?
|
||||
<!--
|
||||
-- Product catalog here:
|
||||
-- https://github.com/sidoh/esp8266_milight_hub/files/1379131/MiLight-ProductCatalog-2017.pdf
|
||||
-->
|
||||
|
||||
### What firmware version(s) have you tried?
|
||||
|
||||
### Which ESP8266 board are you using? (nodemcu, d1_mini, etc.)
|
||||
|
||||
### Which radio type are you using? (RGBW, RGB+CCT, etc.)
|
||||
|
||||
### Have you tried controlling the device with a physical remote?
|
||||
|
||||
<!-- Please provide the model number of the remote if the device you're trying to control works with a remote. -->
|
||||
|
||||
### Output of http://milight-hub.local/about and http://milight-hub.local/settings
|
||||
|
||||
<!-- MAKE SURE TO SENSOR ANY PASSWORDS IN /settings !!! -->
|
||||
|
||||
#### /about
|
||||
|
||||
```json
|
||||
"... /about output. put between the ```s"
|
||||
```
|
||||
|
||||
#### /settings
|
||||
|
||||
```json
|
||||
"... /settings output. put between the ```s"
|
||||
```
|
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
.pioenvs
|
||||
.piolibdeps
|
||||
.pio
|
||||
.clang_complete
|
||||
.gcc-flags.json
|
||||
.sconsign.dblite
|
||||
/web/node_modules
|
||||
/web/build
|
||||
/web/package-lock.json
|
||||
/dist/*.bin
|
||||
/dist/docs
|
||||
.vscode/
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
/test/remote/settings.json
|
||||
/test/remote/espmh.env
|
72
.prepare_docs
Executable file
72
.prepare_docs
Executable file
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# This script sets up API documentation bundles for deployment to Github Pages.
|
||||
# It expects the following structure:
|
||||
#
|
||||
# In development branches:
|
||||
#
|
||||
# * ./docs/openapi.yaml - OpenAPI spec in
|
||||
# * ./docs/gh-pages - Any assets that should be copied to gh-pages root
|
||||
#
|
||||
# In Github Pages, it will generate:
|
||||
#
|
||||
# * ./ - Files from ./docs/gh-pages will be copied
|
||||
# * ./branches/<branch>/... - Deployment bundles including an index.html
|
||||
# and a snapshot of the Open API spec.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
prepare_docs_log() {
|
||||
echo "[prepare docs release] -- $@"
|
||||
}
|
||||
|
||||
# Only run for tagged commits
|
||||
if [ -z "$(git tag -l --points-at HEAD)" ]; then
|
||||
prepare_docs_log "Skipping non-tagged commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DOCS_DIR="./docs"
|
||||
DIST_DIR="./dist/docs"
|
||||
BRANCHES_DIR="${DIST_DIR}/branches"
|
||||
API_SPEC_FILE="${DOCS_DIR}/openapi.yaml"
|
||||
|
||||
rm -rf "${DIST_DIR}"
|
||||
|
||||
redoc_bundle_file=$(mktemp)
|
||||
git_ref_version=$(git describe --always)
|
||||
branch_docs_dir="${BRANCHES_DIR}/${git_ref_version}"
|
||||
|
||||
# Build Redoc bundle (a single HTML file)
|
||||
redoc-cli bundle ${API_SPEC_FILE} -o ${redoc_bundle_file} --title 'Milight Hub API Documentation'
|
||||
|
||||
# Check out current stuff from gh-pages (we'll append to it)
|
||||
git fetch origin 'refs/heads/gh-pages:refs/heads/gh-pages'
|
||||
git checkout gh-pages -- branches || prepare_docs_log "Failed to checkout branches from gh-pages, skipping..."
|
||||
|
||||
if [ -e "./branches" ]; then
|
||||
mkdir -p "${DIST_DIR}"
|
||||
mv "./branches" "${BRANCHES_DIR}"
|
||||
else
|
||||
mkdir -p "${BRANCHES_DIR}"
|
||||
fi
|
||||
|
||||
if [ -e "${DOCS_DIR}/gh-pages" ]; then
|
||||
cp -r ${DOCS_DIR}/gh-pages/* "${DIST_DIR}"
|
||||
else
|
||||
prepare_docs_log "Skipping copy of gh-pages dir, doesn't exist"
|
||||
fi
|
||||
|
||||
# Create the docs bundle for our ref. This will be the redoc bundle + a
|
||||
# snapshot of the OpenAPI spec
|
||||
mkdir -p "${branch_docs_dir}"
|
||||
cp "${API_SPEC_FILE}" "${branch_docs_dir}"
|
||||
cp "${redoc_bundle_file}" "${branch_docs_dir}/index.html"
|
||||
|
||||
# Update `latest` symlink to this branch
|
||||
rm -rf "${BRANCHES_DIR}/latest"
|
||||
ln -s "${git_ref_version}" "${BRANCHES_DIR}/latest"
|
||||
|
||||
# Create a JSON file containing a list of all branches with docs (we'll
|
||||
# have an index page that renders the list).
|
||||
ls "${BRANCHES_DIR}" | jq -Rc '.' | jq -sc '.' > "${DIST_DIR}/branches.json"
|
31
.prepare_release
Executable file
31
.prepare_release
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
prepare_log() {
|
||||
echo "[prepare release] -- $@"
|
||||
}
|
||||
|
||||
if [ -z "$(git tag -l --points-at HEAD)" ]; then
|
||||
prepare_log "Skipping non-tagged commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VERSION=$(git describe)
|
||||
|
||||
prepare_log "Preparing release for tagged version: $VERSION"
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
if [ -d .pio/build ]; then
|
||||
firmware_prefix=".pio/build"
|
||||
else
|
||||
firmware_prefix=".pioenvs"
|
||||
fi
|
||||
|
||||
for file in $(ls ${firmware_prefix}/**/firmware.bin); do
|
||||
env_dir=$(dirname "$file")
|
||||
env=$(basename "$env_dir")
|
||||
|
||||
cp "$file" "dist/esp8266_milight_hub_${env}-${VERSION}.bin"
|
||||
done
|
41
.travis.yml
Normal file
41
.travis.yml
Normal file
|
@ -0,0 +1,41 @@
|
|||
language: python
|
||||
python:
|
||||
- '3.7'
|
||||
sudo: false
|
||||
cache:
|
||||
directories:
|
||||
- "~/.platformio"
|
||||
env:
|
||||
- NODE_VERSION="10"
|
||||
before_install:
|
||||
- nvm install $NODE_VERSION
|
||||
install:
|
||||
- pip3 install -U platformio
|
||||
- platformio lib install
|
||||
- cd web && npm install && cd ..
|
||||
- npm install -g swagger-cli redoc-cli
|
||||
script:
|
||||
- swagger-cli validate ./docs/openapi.yaml
|
||||
- platformio run
|
||||
before_deploy:
|
||||
- ./.prepare_release
|
||||
- ./.prepare_docs
|
||||
deploy:
|
||||
- provider: releases
|
||||
prerelease: true
|
||||
api_key:
|
||||
secure: p1BjM1a/u20EES+pl0+w7B/9600pvpcVYTfMiZhyMOXB0MbNm+uZKYeqiG6Tf3A9duVqMtn0R+ROO+YqL5mlnrVSi74kHMxCIF2GGtK7DIReyEI5JeF5oSi5j9bEsXu8602+1Uez8tInWgzdu2uK2G0FJF/og1Ygnk/L3haYIldIo6kL+Yd6Anlu8L2zqiovC3j3r3eO8oB6Ig6sirN+tnK0ah3dn028k+nHQIMtcc/hE7dQjglp4cGOu+NumUolhdwLdFyW7vfAafxwf9z/SL6M14pg0N8qOmT4KEg4AZQDaKn0wT7VhAvPDHjt4CgPE7QsZhEKFmW7J9LGlcWN4X3ORMkBNPnmqrkVeZEE4Vlcm3CF5kvt59ks0qwEgjpvrqxdZZxa/h9ZLEBBEXMIekA4TSAzP/e/opfry11N1lvqXQ562Jc6oEKS+xWerWSALXyZI4K1T+fkgHTZCWGH4EI3weZY/zSCAZ6a7OpgFQWU9uHlJLMkaWrp78fSPqy6zcjxhXoJnBt8BT1BMRdmZum2YX91hfJ9aRvlEmhtxKgAcPgpJ0ITwB317lKh5VqAfMNZW7pXJEYdLCmUEKXv/beTvNmRIGgu1OjZ3BWchOgh/TwX46+Lrx1zL69sfE+6cBFbC+T2QIv4dxxSQNC1K0JnRVhbD1cOpSXz+amsLS0=
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
file: dist/*.bin
|
||||
on:
|
||||
repo: sidoh/esp8266_milight_hub
|
||||
tags: true
|
||||
- provider: pages
|
||||
skip_cleanup: true
|
||||
local_dir: dist/docs
|
||||
github_token: $GITHUB_TOKEN
|
||||
keep_history: true
|
||||
on:
|
||||
repo: sidoh/esp8266_milight_hub
|
||||
tags: true
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Chris Mullins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
325
README.md
Normal file
325
README.md
Normal file
|
@ -0,0 +1,325 @@
|
|||
# esp8266_milight_hub [![Build Status](https://travis-ci.org/sidoh/esp8266_milight_hub.svg?branch=master)](https://travis-ci.org/sidoh/esp8266_milight_hub) [![License][shield-license]][info-license]
|
||||
|
||||
This is a replacement for a Milight/LimitlessLED remote/gateway hosted on an ESP8266. Leverages [Henryk Plötz's awesome reverse-engineering work](https://hackaday.io/project/5888-reverse-engineering-the-milight-on-air-protocol).
|
||||
|
||||
[Milight bulbs](https://www.amazon.com/Mi-light-Dimmable-RGBWW-Spotlight-Smart/dp/B01LPRQ4BK/r) are cheap smart bulbs that are controllable with an undocumented 2.4 GHz protocol. In order to control them, you either need a [remote](https://www.amazon.com/Mi-light-Dimmable-RGBWW-Spotlight-Smart/dp/B01LCSALV6/r?th=1) ($13), which allows you to control them directly, or a [WiFi gateway](http://futlight.com/productlist.aspx?typeid=125) ($30), which allows you to control them with a mobile app or a [UDP protocol](https://github.com/Fantasmos/LimitlessLED-DevAPI).
|
||||
|
||||
This project is a replacement for the wifi gateway.
|
||||
|
||||
[This guide](http://blog.christophermullins.com/2017/02/11/milight-wifi-gateway-emulator-on-an-esp8266/) on my blog details setting one of these up.
|
||||
|
||||
## Why this is useful
|
||||
|
||||
1. Both the remote and the WiFi gateway are limited to four groups. This means if you want to control more than four groups of bulbs, you need another remote or another gateway. This project allows you to control 262,144 groups (4*2^16, the limit imposed by the protocol).
|
||||
2. This project exposes a nice REST API to control your bulbs.
|
||||
3. You can secure the ESP8266 with a username/password, which is more than you can say for the Milight gateway! (The 2.4 GHz protocol is still totally insecure, so this doesn't accomplish much :).
|
||||
4. Official hubs connect to remote servers to enable WAN access, and this behavior is not disableable.
|
||||
5. This project is capable of passively listening for Milight packets sent from other devices (like remotes). It can publish data from intercepted packets to MQTT. This could, for example, allow the use of Milight remotes while keeping your home automation platform's state in sync. See the MQTT section for more detail.
|
||||
|
||||
## Supported remotes
|
||||
|
||||
The following remotes can be emulated:
|
||||
|
||||
Support has been added for the following [bulb types](http://futlight.com/productlist.aspx?typeid=101):
|
||||
|
||||
Model #|Name|Compatible Bulbs
|
||||
-------|-----------|----------------
|
||||
|FUT096|RGB/W|<ol><li>FUT014</li><li>FUT016</li><li>FUT103</li>|
|
||||
|FUT005<br/>FUT006<br/>FUT007</li></ol>|CCT|<ol><li>FUT011</li><li>FUT017</li><li>FUT019</li></ol>|
|
||||
|FUT098|RGB|Most RGB LED Strip Controlers|
|
||||
|FUT020|RGB|Some other RGB LED strip controllers|
|
||||
|FUT092|RGB/CCT|<ol><li>FUT012</li><li>FUT013</li><li>FUT014</li><li>FUT015</li><li>FUT103</li><li>FUT104</li><li>FUT105</li><li>Many RGB/CCT LED Strip Controllers</li></ol>|
|
||||
|FUT091|CCT v2|Most newer dual white bulbs and controllers|
|
||||
|FUT089|8-zone RGB/CCT|Most newer rgb + dual white bulbs and controllers|
|
||||
|
||||
Other remotes or bulbs, but have not been tested.
|
||||
|
||||
## What you'll need
|
||||
|
||||
1. An ESP8266. I used a NodeMCU.
|
||||
2. A NRF24L01+ module (~$3 on ebay). Alternatively, you can use a LT8900.
|
||||
3. Some way to connect the two (7 female/female dupont cables is probably easiest).
|
||||
|
||||
## Installing
|
||||
|
||||
#### Connect the NRF24L01+ / LT8900
|
||||
|
||||
This project is compatible with both NRF24L01 and LT8900 radios. LT8900 is the same model used in the official MiLight devices. NRF24s are a very common 2.4 GHz radio device, but require software emulation of the LT8900's packet structure. As such, the LT8900 is more performant.
|
||||
|
||||
Both modules are SPI devices and should be connected to the standard SPI pins on the ESP8266.
|
||||
|
||||
##### NRF24L01+
|
||||
|
||||
|
||||
[This guide](https://www.mysensors.org/build/connect_radio#nrf24l01+-&-esp8266) details how to connect an NRF24 to an ESP8266. By default GPIO 4 for CE and GPIO 15 for CSN are used, but these can be configured late in the Web GUI under Settings -> Setup.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/40266/47967518-67556f00-e05e-11e8-857d-1173a9da955c.png" align="left" width="32%" />
|
||||
<img src="https://user-images.githubusercontent.com/40266/47967520-691f3280-e05e-11e8-838a-83706df2edb0.png" align="left" width="22%" />
|
||||
|
||||
NodeMCU | Radio | Color
|
||||
-- | -- | --
|
||||
GND | GND | Black
|
||||
3V3 | VCC | Red
|
||||
D2 (GPIO4) | CE | Orange
|
||||
D8 (GPIO15) | CSN/CS | Yellow
|
||||
D5 (GPIO14) | SCK | Green
|
||||
D7 (GPIO13) | MOSI | Blue
|
||||
D6 (GPIO12) | MISO | Violet
|
||||
|
||||
_Image source: [MySensors.org](https://mysensors.org)_
|
||||
|
||||
|
||||
##### LT8900
|
||||
|
||||
Connect SPI pins (CE, SCK, MOSI, MISO) to appropriate SPI pins on the ESP8266. With default settings, connect RST to GPIO 0, PKT to GPIO 16, CE to GPIO 4, and CSN to GPIO 15. Make sure to properly configure these if using non-default pinouts.
|
||||
|
||||
#### Setting up the ESP
|
||||
|
||||
The goal here is to flash your ESP with the firmware. It's really easy to do this with [PlatformIO](http://platformio.org/):
|
||||
|
||||
```
|
||||
export ESP_BOARD=nodemcuv2
|
||||
platformio run -e $ESP_BOARD --target upload
|
||||
```
|
||||
|
||||
Of course make sure to substitute `nodemcuv2` with the board that you're using.
|
||||
|
||||
You can find pre-compiled firmware images on the [releases](https://github.com/sidoh/esp8266_milight_hub/releases).
|
||||
|
||||
#### Configure WiFi
|
||||
|
||||
This project uses [WiFiManager](https://github.com/tzapu/WiFiManager) to avoid the need to hardcode AP credentials in the firmware.
|
||||
|
||||
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. If your board has a built-in LED (or you wire up an LED), it will [flash to indicate the status](#led-status).
|
||||
|
||||
The network password is "**milightHub**".
|
||||
|
||||
#### Get IP Address
|
||||
|
||||
Both mDNS and SSDP are supported.
|
||||
|
||||
* OS X - you should be able to navigate to http://milight-hub.local.
|
||||
* Windows - you should see a device called "ESP8266 MiLight Gateway" show up in your network explorer.
|
||||
* Linux users can install [avahi](http://www.avahi.org/) (`sudo apt-get install avahi-daemon` on Ubuntu), and should then be able to navigate to http://milight-hub.local.
|
||||
|
||||
#### 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>`, or `http://milight-hub.local` if your client supports mDNS. The UI should look like this:
|
||||
|
||||
![Web UI](https://user-images.githubusercontent.com/589893/61682228-a8151700-acc5-11e9-8b86-1e21efa6cdbe.png)
|
||||
|
||||
|
||||
If it does not work as expected see [Troubleshooting](https://github.com/sidoh/esp8266_milight_hub/wiki/Troubleshooting).
|
||||
|
||||
#### Pair Bulbs
|
||||
|
||||
If you need to pair some bulbs, how to do this is [described in the wiki](https://github.com/sidoh/esp8266_milight_hub/wiki/Pairing-new-bulbs).
|
||||
|
||||
## Device Aliases
|
||||
|
||||
You can configure aliases or labels for a given _(Device Type, Device ID, Group ID)_ tuple. For example, you might want to call the RGB+CCT remote with the ID `0x1111` and the Group ID `1` to be called `living_room`. Aliases are useful in a couple of different ways:
|
||||
|
||||
* **In the UI**: the aliases dropdown shows all previously set aliases. When one is selected, the corresponding Device ID, Device Type, and Group ID are selected. This allows you to not need to memorize the ID parameters for each lighting device if you're controlling them through the UI.
|
||||
* **In the REST API**: standard CRUD verbs (`GET`, `PUT`, and `DELETE`) allow you to interact with aliases via the `/gateways/:device_alias` route.
|
||||
* **MQTT**: you can configure topics to listen for commands and publish updates/state using aliases rather than IDs.
|
||||
|
||||
## REST API
|
||||
|
||||
The REST API is specified using the [OpenAPI v3](https://swagger.io/docs/specification/about/) specification.
|
||||
|
||||
[openapi.yaml](docs/openapi.yaml) contains the raw spec.
|
||||
|
||||
[You can view generated documentation for the master branch here.](https://sidoh.github.io/esp8266_milight_hub/branches/latest)
|
||||
|
||||
[Docs for other branches can be found here](https://sidoh.github.io/esp8266_milight_hub)
|
||||
|
||||
## MQTT
|
||||
|
||||
To configure your ESP to integrate with MQTT, fill out the following settings:
|
||||
|
||||
1. `mqtt_server`- IP or hostname should work. Specify a port with standard syntax (e.g., "mymqttbroker.com:1884").
|
||||
1. `mqtt_topic_pattern` - you can control arbitrary configurations of device ID, device type, and group ID with this. A good default choice is something like `milight/:device_id/:device_type/:group_id`. More detail is provided below.
|
||||
1. (optionally) `mqtt_username`
|
||||
1. (optionally) `mqtt_password`
|
||||
|
||||
#### More detail on `mqtt_topic_pattern`
|
||||
|
||||
`mqtt_topic_pattern` leverages single-level wildcards (documented [here](https://mosquitto.org/man/mqtt-7.html)). For example, specifying `milight/:device_id/:device_type/:group_id` will cause the ESP to subscribe to the topic `milight/+/+/+`. It will then interpret the second, third, and fourth tokens in topics it receives messages on as `:device_id`, `:device_type`, and `:group_id`, respectively. The following tokens are available:
|
||||
|
||||
1. `:device_id` - Device ID. Can be hexadecimal (e.g. `0x1234`) or decimal (e.g. `4660`).
|
||||
1. `:device_type` - Remote type. `rgbw`, `fut089`, etc.
|
||||
1. `:group_id` - Group. 0-4 for most remotes. The "All" group is group 0.
|
||||
1. `:device_alias` - Alias for the given device. Note that if an alias is not configured, a default token `__unnamed_group` will be substituted instead.
|
||||
|
||||
Messages should be JSON objects using exactly the same schema that the [REST gateway](https://sidoh.github.io/esp8266_milight_hub/branches/latest/#tag/Device-Control/paths/~1gateways~1{device-id}~1{remote-type}~1{group-id}/put) uses for the `/gateways/:device_id/:device_type/:group_id` endpoint.
|
||||
|
||||
#### Example:
|
||||
|
||||
If `mqtt_topic_pattern` is set to `milight/:device_id/:device_type/:group_id`, you could send the following message to it (the below example uses a ruby MQTT client):
|
||||
|
||||
```ruby
|
||||
irb(main):001:0> require 'mqtt'
|
||||
irb(main):002:0> client = MQTT::Client.new('10.133.8.11',1883)
|
||||
irb(main):003:0> client.connect
|
||||
irb(main):004:0> client.publish('milight/0x118D/rgb_cct/1', '{"status":"ON","color":{"r":255,"g":200,"b":255},"brightness":100}')
|
||||
```
|
||||
|
||||
This will instruct the ESP to send messages to RGB+CCT bulbs with device ID `0x118D` in group 1 to turn on, set color to RGB(255,200,255), and brightness to 100.
|
||||
|
||||
#### Updates
|
||||
|
||||
ESPMH is capable of providing two types of updates:
|
||||
|
||||
1. Delta: as packets are received, they are translated into the corresponding command (e.g., "set brightness to 50"). The translated command is sent as an update.
|
||||
2. State: When an update is received, the corresponding command is applied to known group state, and the whole state for the group is transmitted.
|
||||
|
||||
##### Delta updates
|
||||
|
||||
To publish data from intercepted packets to an MQTT topic, configure MQTT server settings, and set the `mqtt_update_topic_pattern` to something of your choice. As with `mqtt_topic_pattern`, the tokens `:device_id`, `:device_type`, and `:group_id` will be substituted with the values from the relevant packet. `:device_id` will always be substituted with the hexadecimal value of the ID. You can also use `:hex_device_id`, or `:dec_device_id` if you prefer decimal.
|
||||
|
||||
The published message is a JSON blob containing the state that was changed.
|
||||
|
||||
As an example, if `mqtt_update_topic_pattern` is set to `milight/updates/:hex_device_id/:device_type/:group_id`, and the group 1 on button of a Milight remote is pressed, the following update will be dispatched:
|
||||
|
||||
```ruby
|
||||
irb(main):005:0> client.subscribe('milight/updates/+/+/+')
|
||||
=> 27
|
||||
irb(main):006:0> puts client.get.inspect
|
||||
["lights/updates/0x1C8E/rgb_cct/1", "{\"status\":\"on\"}"]
|
||||
```
|
||||
|
||||
##### Full state updates
|
||||
|
||||
For this mode, `mqtt_state_topic_pattern` should be set to something like `milight/states/:hex_device_id/:device_type/:group_id`. As an example:
|
||||
|
||||
```ruby
|
||||
irb(main):005:0> client.subscribe('milight/states/+/+/+')
|
||||
=> 27
|
||||
irb(main):006:0> puts client.get.inspect
|
||||
["lights/states/0x1C8E/rgb_cct/1", "{\"state\":\"ON\",\"brightness\":255,\"color_temp\":370,\"bulb_mode\":\"white\"}"]
|
||||
irb(main):007:0> puts client.get.inspect
|
||||
["lights/states/0x1C8E/rgb_cct/1", "{\"state\":\"ON\",\"brightness\":100,\"color_temp\":370,\"bulb_mode\":\"white\"}"]
|
||||
```
|
||||
|
||||
**Make sure that `mqtt_topic_pattern`, `mqtt_state_topic_pattern`, and `matt_update_topic_pattern` are all different!** If they are they same you can put your ESP in a loop where its own updates trigger an infinite command loop.
|
||||
|
||||
##### Customize fields
|
||||
|
||||
You can select which fields should be included in state updates by configuring the `group_state_fields` parameter. Available fields should be mostly self explanatory, but are all documented in the REST API spec under `GroupStateField`.
|
||||
|
||||
#### Client Status
|
||||
|
||||
To receive updates when the MQTT client connects or disconnects from the broker, confugre the `mqtt_client_status_topic` parameter. A message of the following form will be published:
|
||||
|
||||
```json
|
||||
{"status":"disconnected_unclean","firmware":"milight-hub","version":"1.9.0-rc3","ip_address":"192.168.1.111","reset_reason":"External System"}
|
||||
```
|
||||
|
||||
If you wish to have the simple messages `connected` and `disconnected` instead of the above environmental data, configure `simple_mqtt_client_status` to `true` (or set Client Status Message Mode to "Simple" in the Web UI).
|
||||
|
||||
## 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](https://github.com/BKrajancic/LimitlessLED-DevAPI/)). 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.
|
||||
|
||||
## Transitions
|
||||
|
||||
Transitions between two given states are supported. Depending on how transition commands are being issued, the duration and smoothness of the transition are both configurable. There are a few ways to use transitions:
|
||||
|
||||
#### RESTful `/transitions` routes
|
||||
|
||||
These routes are fully documented in the [REST API documentation](https://sidoh.github.io/esp8266_milight_hub/branches/latest/#tag/Transitions).
|
||||
|
||||
#### `transition` field when issuing commands
|
||||
|
||||
When you issue a command to a bulb either via REST or MQTT, you can include a `transition` field. The value of this field specifies the duration of the transition, in seconds (non-integer values are supported).
|
||||
|
||||
For example, the command:
|
||||
|
||||
```json
|
||||
{"brightness":255,"transition":60}
|
||||
```
|
||||
|
||||
will transition from whatever the current brightness is to `brightness=255` over 60 seconds.
|
||||
|
||||
#### Notes on transitions
|
||||
|
||||
* espMH's transitions should work seamlessly with [HomeAssistant's transition functionality](https://www.home-assistant.io/components/light/).
|
||||
* You can issue commands specifying transitions between many fields at once. For example:
|
||||
```json
|
||||
{"brightness":255,"kelvin":0,"transition":10.5}
|
||||
```
|
||||
will transition from current values for brightness and kelvin to the specified values -- 255 and 0 respectively -- over 10.5 seconds.
|
||||
* Color transitions are supported. Under the hood, this is treated as a transition between current values for r, g, and b to the r, g, b values for the specified color. Because milight uses hue-sat colors, this might not behave exactly as you'd expect for all colors.
|
||||
* You can transition to a given `status` or `state`. For example,
|
||||
```json
|
||||
{"status":"ON","transition":10}
|
||||
```
|
||||
will turn the bulb on, immediately set the brightness to 0, and then transition to brightness=255 over 10 seconds. If you specify a brightness value, the transition will stop there instead of 255.
|
||||
|
||||
## LED Status
|
||||
|
||||
Some ESP boards have a built-in LED, on pin #2. This LED will flash to indicate the current status of the hub:
|
||||
|
||||
* Wifi not configured: Fast flash (on/off once per second). See [Configure Wifi](#configure-wifi) to configure the hub.
|
||||
* Wifi connected and ready: Occasional blips of light (a flicker of light every 1.5 seconds).
|
||||
* Packets sending/receiving: Rapid blips of light for brief periods (three rapid flashes).
|
||||
* Wifi failed to configure: Solid light.
|
||||
|
||||
In the setup UI, you can turn on "enable_solid_led" to change the LED behavior to:
|
||||
|
||||
* Wifi connected and ready: Solid LED light
|
||||
* Wifi failed to configure: Light off
|
||||
|
||||
Note that you must restart the hub to affect the change in "enable_solid_led".
|
||||
|
||||
You can configure the LED pin from the web console. Note that pin means the GPIO number, not the D number ... for example, D1 is actually GPIO5 and therefore its pin 5. If you specify the pin as a negative number, it will invert the LED signal (the built-in LED on pin 2 (D4) is inverted, so the default is -2).
|
||||
|
||||
If you want to wire up your own LED you can connect it to D1/GPIO5. Put a wire from D1 to one side of a 220 ohm resistor. On the other side, connect it to the positive side (the longer wire) of a 3.3V LED. Then connect the negative side of the LED (the shorter wire) to ground. If you use a different voltage LED, or a high current LED, you will need to add a driver circuit.
|
||||
|
||||
Another option is to use an external LED parallel to the (inverted) internal one, this way it will mirror the internal LED without configuring a new LED pin in the UI. To do this connect the (short) GND pin of your LED to D4. The longer one to a 220 ohm resistor and finally the other side of the resistor to a 3V3 pin.
|
||||
|
||||
## Development
|
||||
|
||||
This project is developed and built using [PlatformIO](https://platformio.org/).
|
||||
|
||||
#### Running tests
|
||||
|
||||
On-board unit tests are available using PlatformIO. Run unit tests with this command:
|
||||
|
||||
```
|
||||
pio test -e d1_mini
|
||||
```
|
||||
|
||||
substituting `d1_mini` for the environment of your choice.
|
||||
|
||||
#### Running integration tests
|
||||
|
||||
A remote integration test suite built using rspec is available under [`./test/remote`](test/remote).
|
||||
|
||||
## Ready-Made Hub
|
||||
|
||||
h4nc (h4nc.zigbee(a)gmail.com) created a PCB and 3D-printable case for espMH. He's offering ready-made versions. Please get in touch with him at the aforementioned email address for further information.
|
||||
|
||||
Find more information from the [espmh_pcb](https://github.com/sidoh/espmh_pcb) repository.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
* @WoodsterDK added support for LT8900 radios.
|
||||
* @cmidgley contributed many substantial features to the 1.7 release.
|
||||
|
||||
[info-license]: https://github.com/sidoh/esp8266_milight_hub/blob/master/LICENSE
|
||||
[shield-license]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
|
||||
## Donating
|
||||
|
||||
If the project brings you happiness or utility, it's more than enough for me to hear those words.
|
||||
|
||||
If you're feeling especially generous, and are open to a charitable donation, that'd make me very happy. Here are some whose mission I support (in no particular order):
|
||||
|
||||
* [Water.org](https://www.water.org)
|
||||
* [Brain & Behavior Research Foundation](https://www.bbrfoundation.org/)
|
||||
* [Electronic Frontier Foundation](https://www.eff.org/)
|
||||
* [Girls Who Code](https://girlswhocode.com/)
|
||||
* [San Francisco Animal Care & Control](http://www.sfanimalcare.org/make-a-donation/)
|
2
dist/index.html.gz.h
vendored
Normal file
2
dist/index.html.gz.h
vendored
Normal file
File diff suppressed because one or more lines are too long
73
docs/gh-pages/index.html
Normal file
73
docs/gh-pages/index.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title></title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<style>
|
||||
thead th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
th, td {
|
||||
padding: 0.5em 1em 0 1em;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
|
||||
<script>
|
||||
var generateDocsRow = function(x) {
|
||||
var html = '<tr><td>' + x + '</td>';
|
||||
html += '<td><a href="branches/' + x + '">Docs</a></td>';
|
||||
html += '<td><a href="branches/' + x + '/openapi.yaml">OpenAPI spec</a></td></tr>';
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
$(function() {
|
||||
$.getJSON(
|
||||
'branches.json',
|
||||
function(data) {
|
||||
var list = $('#version-list');
|
||||
var html = '<thead><th>Version</th><th></th><th></th></thead><tbody>';
|
||||
|
||||
html += generateDocsRow('latest');
|
||||
|
||||
data.forEach(function(x) {
|
||||
if (x != 'latest') {
|
||||
html += generateDocsRow(x);
|
||||
}
|
||||
});
|
||||
|
||||
html += '</tbody>'
|
||||
|
||||
list.append(html);
|
||||
$('#loading').hide();
|
||||
},
|
||||
function(err) {
|
||||
console.log(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 7]>
|
||||
<p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
|
||||
<h2>MiLight Hub REST API Documentation</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
<table id="version-list"></table>
|
||||
<i id="loading">Loading...</i>
|
||||
</body>
|
||||
</html>
|
1057
docs/openapi.yaml
Normal file
1057
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load diff
336
lib/DataStructures/LinkedList.h
Normal file
336
lib/DataStructures/LinkedList.h
Normal file
|
@ -0,0 +1,336 @@
|
|||
/*
|
||||
********* Adapted from: *********
|
||||
https://github.com/ivanseidel/LinkedList
|
||||
Created by Ivan Seidel Gomes, March, 2013.
|
||||
Released into the public domain.
|
||||
*********************************
|
||||
|
||||
Changes:
|
||||
- public access to ListNode (allows for splicing for LRU)
|
||||
- doubly-linked
|
||||
- remove caching stuff in favor of standard linked list iterating
|
||||
- remove sorting
|
||||
*/
|
||||
|
||||
#ifndef LinkedList_h
|
||||
#define LinkedList_h
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
template<class T>
|
||||
struct ListNode {
|
||||
T data;
|
||||
ListNode<T> *next;
|
||||
ListNode<T> *prev;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class LinkedList {
|
||||
|
||||
protected:
|
||||
size_t _size;
|
||||
ListNode<T> *root;
|
||||
ListNode<T> *last;
|
||||
|
||||
public:
|
||||
LinkedList();
|
||||
~LinkedList();
|
||||
|
||||
/*
|
||||
Returns current size of LinkedList
|
||||
*/
|
||||
virtual size_t size() const;
|
||||
/*
|
||||
Adds a T object in the specified index;
|
||||
Unlink and link the LinkedList correcly;
|
||||
Increment _size
|
||||
*/
|
||||
virtual bool add(int index, T);
|
||||
/*
|
||||
Adds a T object in the end of the LinkedList;
|
||||
Increment _size;
|
||||
*/
|
||||
virtual bool add(T);
|
||||
/*
|
||||
Adds a T object in the start of the LinkedList;
|
||||
Increment _size;
|
||||
*/
|
||||
virtual bool unshift(T);
|
||||
/*
|
||||
Set the object at index, with T;
|
||||
Increment _size;
|
||||
*/
|
||||
virtual bool set(int index, T);
|
||||
/*
|
||||
Remove object at index;
|
||||
If index is not reachable, returns false;
|
||||
else, decrement _size
|
||||
*/
|
||||
virtual T remove(int index);
|
||||
virtual void remove(ListNode<T>* node);
|
||||
/*
|
||||
Remove last object;
|
||||
*/
|
||||
virtual T pop();
|
||||
/*
|
||||
Remove first object;
|
||||
*/
|
||||
virtual T shift();
|
||||
/*
|
||||
Get the index'th element on the list;
|
||||
Return Element if accessible,
|
||||
else, return false;
|
||||
*/
|
||||
virtual T get(int index);
|
||||
|
||||
/*
|
||||
Clear the entire array
|
||||
*/
|
||||
virtual void clear();
|
||||
|
||||
ListNode<T>* getNode(int index);
|
||||
virtual void spliceToFront(ListNode<T>* node);
|
||||
ListNode<T>* getHead() { return root; }
|
||||
T getLast() const { return last == NULL ? T() : last->data; }
|
||||
|
||||
};
|
||||
|
||||
|
||||
template<typename T>
|
||||
void LinkedList<T>::spliceToFront(ListNode<T>* node) {
|
||||
// Node is already root
|
||||
if (node->prev == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
node->prev->next = node->next;
|
||||
if (node->next != NULL) {
|
||||
node->next->prev = node->prev;
|
||||
} else {
|
||||
last = node->prev;
|
||||
}
|
||||
|
||||
root->prev = node;
|
||||
node->next = root;
|
||||
node->prev = NULL;
|
||||
root = node;
|
||||
}
|
||||
|
||||
// Initialize LinkedList with false values
|
||||
template<typename T>
|
||||
LinkedList<T>::LinkedList()
|
||||
{
|
||||
root=NULL;
|
||||
last=NULL;
|
||||
_size=0;
|
||||
}
|
||||
|
||||
// Clear Nodes and free Memory
|
||||
template<typename T>
|
||||
LinkedList<T>::~LinkedList()
|
||||
{
|
||||
ListNode<T>* tmp;
|
||||
while(root!=NULL)
|
||||
{
|
||||
tmp=root;
|
||||
root=root->next;
|
||||
delete tmp;
|
||||
}
|
||||
last = NULL;
|
||||
_size=0;
|
||||
}
|
||||
|
||||
/*
|
||||
Actualy "logic" coding
|
||||
*/
|
||||
|
||||
template<typename T>
|
||||
ListNode<T>* LinkedList<T>::getNode(int index){
|
||||
|
||||
int _pos = 0;
|
||||
ListNode<T>* current = root;
|
||||
|
||||
while(_pos < index && current){
|
||||
current = current->next;
|
||||
|
||||
_pos++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
size_t LinkedList<T>::size() const{
|
||||
return _size;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool LinkedList<T>::add(int index, T _t){
|
||||
|
||||
if(index >= _size)
|
||||
return add(_t);
|
||||
|
||||
if(index == 0)
|
||||
return unshift(_t);
|
||||
|
||||
ListNode<T> *tmp = new ListNode<T>(),
|
||||
*_prev = getNode(index-1);
|
||||
tmp->data = _t;
|
||||
tmp->next = _prev->next;
|
||||
_prev->next = tmp;
|
||||
|
||||
_size++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool LinkedList<T>::add(T _t){
|
||||
|
||||
ListNode<T> *tmp = new ListNode<T>();
|
||||
tmp->data = _t;
|
||||
tmp->next = NULL;
|
||||
|
||||
if(root){
|
||||
// Already have elements inserted
|
||||
last->next = tmp;
|
||||
tmp->prev = last;
|
||||
last = tmp;
|
||||
}else{
|
||||
// First element being inserted
|
||||
root = tmp;
|
||||
last = tmp;
|
||||
}
|
||||
|
||||
_size++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool LinkedList<T>::unshift(T _t){
|
||||
|
||||
if(_size == 0)
|
||||
return add(_t);
|
||||
|
||||
ListNode<T> *tmp = new ListNode<T>();
|
||||
tmp->next = root;
|
||||
root->prev = tmp;
|
||||
tmp->data = _t;
|
||||
root = tmp;
|
||||
|
||||
_size++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool LinkedList<T>::set(int index, T _t){
|
||||
// Check if index position is in bounds
|
||||
if(index < 0 || index >= _size)
|
||||
return false;
|
||||
|
||||
getNode(index)->data = _t;
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T LinkedList<T>::pop(){
|
||||
if(_size <= 0)
|
||||
return T();
|
||||
|
||||
if(_size >= 2){
|
||||
ListNode<T> *tmp = last->prev;
|
||||
T ret = tmp->next->data;
|
||||
delete(tmp->next);
|
||||
tmp->next = NULL;
|
||||
last = tmp;
|
||||
_size--;
|
||||
return ret;
|
||||
}else{
|
||||
// Only one element left on the list
|
||||
T ret = root->data;
|
||||
delete(root);
|
||||
root = NULL;
|
||||
last = NULL;
|
||||
_size = 0;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T LinkedList<T>::shift(){
|
||||
if(_size <= 0)
|
||||
return T();
|
||||
|
||||
if(_size > 1){
|
||||
ListNode<T> *_next = root->next;
|
||||
T ret = root->data;
|
||||
delete(root);
|
||||
root = _next;
|
||||
_size --;
|
||||
|
||||
return ret;
|
||||
}else{
|
||||
// Only one left, then pop()
|
||||
return pop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void LinkedList<T>::remove(ListNode<T>* node){
|
||||
if (node == root) {
|
||||
shift();
|
||||
} else if (node == last) {
|
||||
pop();
|
||||
} else {
|
||||
ListNode<T>* prev = node->prev;
|
||||
ListNode<T>* next = node->next;
|
||||
|
||||
prev->next = next;
|
||||
next->prev = prev;
|
||||
|
||||
delete node;
|
||||
--_size;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T LinkedList<T>::remove(int index){
|
||||
if (index < 0 || index >= _size)
|
||||
{
|
||||
return T();
|
||||
}
|
||||
|
||||
if(index == 0)
|
||||
return shift();
|
||||
|
||||
if (index == _size-1)
|
||||
{
|
||||
return pop();
|
||||
}
|
||||
|
||||
ListNode<T> *tmp = getNode(index - 1);
|
||||
ListNode<T> *toDelete = tmp->next;
|
||||
T ret = toDelete->data;
|
||||
tmp->next = tmp->next->next;
|
||||
delete(toDelete);
|
||||
_size--;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
T LinkedList<T>::get(int index){
|
||||
ListNode<T> *tmp = getNode(index);
|
||||
|
||||
return (tmp ? tmp->data : T());
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void LinkedList<T>::clear(){
|
||||
while(size() > 0)
|
||||
shift();
|
||||
}
|
||||
#endif
|
73
lib/Helpers/IntParsing.h
Normal file
73
lib/Helpers/IntParsing.h
Normal file
|
@ -0,0 +1,73 @@
|
|||
#ifndef _INTPARSING_H
|
||||
#define _INTPARSING_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
template <typename T>
|
||||
const T strToHex(const char* s, size_t length) {
|
||||
T value = 0;
|
||||
T base = 1;
|
||||
|
||||
for (int i = length-1; i >= 0; i--) {
|
||||
const char c = s[i];
|
||||
|
||||
if (c >= '0' && c <= '9') {
|
||||
value += ((c - '0') * base);
|
||||
} else if (c >= 'a' && c <= 'f') {
|
||||
value += ((c - 'a' + 10) * base);
|
||||
} else if (c >= 'A' && c <= 'F') {
|
||||
value += ((c - 'A' + 10) * base);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
base <<= 4;
|
||||
}
|
||||
|
||||
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")) {
|
||||
return strToHex<T>(s.substring(2));
|
||||
} else {
|
||||
return s.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IntParsing {
|
||||
public:
|
||||
static void bytesToHexStr(const uint8_t* bytes, const size_t len, char* buffer, size_t maxLen) {
|
||||
char* p = buffer;
|
||||
|
||||
for (size_t i = 0; i < len && static_cast<size_t>(p - buffer) < (maxLen - 3); i++) {
|
||||
p += sprintf(p, "%02X", bytes[i]);
|
||||
|
||||
if (i < (len - 1)) {
|
||||
p += sprintf(p, " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
51
lib/Helpers/JsonHelpers.h
Normal file
51
lib/Helpers/JsonHelpers.h
Normal file
|
@ -0,0 +1,51 @@
|
|||
#include <ArduinoJson.h>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
|
||||
#ifndef _JSON_HELPERS_H
|
||||
#define _JSON_HELPERS_H
|
||||
|
||||
class JsonHelpers {
|
||||
public:
|
||||
template<typename T>
|
||||
static void copyFrom(JsonArray arr, std::vector<T> vec) {
|
||||
for (typename std::vector<T>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
|
||||
arr.add(*it);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static void copyTo(JsonArray arr, std::vector<T> vec) {
|
||||
for (size_t i = 0; i < arr.size(); ++i) {
|
||||
JsonVariant val = arr[i];
|
||||
vec.push_back(val.as<T>());
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T, typename StrType>
|
||||
static std::vector<T> jsonArrToVector(JsonArray& arr, std::function<T (const StrType)> converter, const bool unique = true) {
|
||||
std::vector<T> vec;
|
||||
|
||||
for (size_t i = 0; i < arr.size(); ++i) {
|
||||
StrType strVal = arr[i];
|
||||
T convertedVal = converter(strVal);
|
||||
|
||||
// inefficient, but everything using this is tiny, so doesn't matter
|
||||
if (!unique || std::find(vec.begin(), vec.end(), convertedVal) == vec.end()) {
|
||||
vec.push_back(convertedVal);
|
||||
}
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
template<typename T, typename StrType>
|
||||
static void vectorToJsonArr(JsonArray& arr, const std::vector<T>& vec, std::function<StrType (const T&)> converter) {
|
||||
for (typename std::vector<T>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
|
||||
arr.add(converter(*it));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
11
lib/Helpers/Size.h
Normal file
11
lib/Helpers/Size.h
Normal file
|
@ -0,0 +1,11 @@
|
|||
#include <Arduino.h>
|
||||
|
||||
#ifndef _SIZE_H
|
||||
#define _SIZE_H
|
||||
|
||||
template<typename T, size_t sz>
|
||||
size_t size(T(&)[sz]) {
|
||||
return sz;
|
||||
}
|
||||
|
||||
#endif
|
32
lib/Helpers/Units.h
Normal file
32
lib/Helpers/Units.h
Normal file
|
@ -0,0 +1,32 @@
|
|||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#ifndef _UNITS_H
|
||||
#define _UNITS_H
|
||||
|
||||
// MiLight CCT bulbs range from 2700K-6500K, or ~370.3-153.8 mireds.
|
||||
#define COLOR_TEMP_MAX_MIREDS 370
|
||||
#define COLOR_TEMP_MIN_MIREDS 153
|
||||
|
||||
class Units {
|
||||
public:
|
||||
template <typename T, typename V>
|
||||
static T rescale(T value, V newMax, float oldMax = 255.0) {
|
||||
return round(value * (newMax / oldMax));
|
||||
}
|
||||
|
||||
static uint8_t miredsToWhiteVal(uint16_t mireds, uint8_t maxValue = 255) {
|
||||
return rescale<uint16_t, uint16_t>(
|
||||
constrain(mireds, COLOR_TEMP_MIN_MIREDS, COLOR_TEMP_MAX_MIREDS) - COLOR_TEMP_MIN_MIREDS,
|
||||
maxValue,
|
||||
(COLOR_TEMP_MAX_MIREDS - COLOR_TEMP_MIN_MIREDS)
|
||||
);
|
||||
}
|
||||
|
||||
static uint16_t whiteValToMireds(uint8_t value, uint8_t maxValue = 255) {
|
||||
uint16_t scaled = rescale<uint16_t, uint16_t>(value, (COLOR_TEMP_MAX_MIREDS - COLOR_TEMP_MIN_MIREDS), maxValue);
|
||||
return COLOR_TEMP_MIN_MIREDS + scaled;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
225
lib/LEDStatus/LEDStatus.cpp
Normal file
225
lib/LEDStatus/LEDStatus.cpp
Normal file
|
@ -0,0 +1,225 @@
|
|||
#include <LEDStatus.h>
|
||||
|
||||
// constructor defines which pin the LED is attached to
|
||||
LEDStatus::LEDStatus(int8_t ledPin) {
|
||||
// if pin negative, reverse and set inverse on pin outputs
|
||||
if (ledPin < 0) {
|
||||
ledPin = -ledPin;
|
||||
_inverse = true;
|
||||
} else {
|
||||
_inverse = false;
|
||||
}
|
||||
// set up the pin
|
||||
_ledPin = ledPin;
|
||||
pinMode(_ledPin, OUTPUT);
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
_timer = millis();
|
||||
}
|
||||
|
||||
// change pin at runtime
|
||||
void LEDStatus::changePin(int8_t ledPin) {
|
||||
bool inverse;
|
||||
// if pin negative, reverse and set inverse on pin outputs
|
||||
if (ledPin < 0) {
|
||||
ledPin = -ledPin;
|
||||
inverse = true;
|
||||
} else {
|
||||
inverse = false;
|
||||
}
|
||||
|
||||
if ((ledPin != _ledPin) || (inverse != _inverse)) {
|
||||
// make sure old pin is off
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
_ledPin = ledPin;
|
||||
_inverse = inverse;
|
||||
// and make sure new pin is also off
|
||||
pinMode(_ledPin, OUTPUT);
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// identify how to flash the LED by mode, continuously until changed
|
||||
void LEDStatus::continuous(LEDStatus::LEDMode mode) {
|
||||
uint16_t ledOffMs, ledOnMs;
|
||||
_modeToTime(mode, ledOffMs, ledOnMs);
|
||||
continuous(ledOffMs, ledOnMs);
|
||||
}
|
||||
|
||||
// identify how to flash the LED by on/off times (in ms), continuously until changed
|
||||
void LEDStatus::continuous(uint16_t ledOffMs, uint16_t ledOnMs) {
|
||||
_continuousOffMs = ledOffMs;
|
||||
_continuousOnMs = ledOnMs;
|
||||
_continuousCurrentlyOn = false;
|
||||
// reset LED to off
|
||||
if (_ledPin > 0) {
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
}
|
||||
// restart timer
|
||||
_timer = millis();
|
||||
}
|
||||
|
||||
// identify a one-shot LED action (overrides continuous until done) by mode
|
||||
void LEDStatus::oneshot(LEDStatus::LEDMode mode, uint8_t count) {
|
||||
uint16_t ledOffMs, ledOnMs;
|
||||
_modeToTime(mode, ledOffMs, ledOnMs);
|
||||
oneshot(ledOffMs, ledOnMs, count);
|
||||
}
|
||||
|
||||
// identify a one-shot LED action (overrides continuous until done) by times (in ms)
|
||||
void LEDStatus::oneshot(uint16_t ledOffMs, uint16_t ledOnMs, uint8_t count) {
|
||||
_oneshotOffMs = ledOffMs;
|
||||
_oneshotOnMs = ledOnMs;
|
||||
_oneshotCountRemaining = count;
|
||||
_oneshotCurrentlyOn = false;
|
||||
// reset LED to off
|
||||
if (_ledPin > 0) {
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
}
|
||||
// restart timer
|
||||
_timer = millis();
|
||||
}
|
||||
|
||||
// call this function in your loop - it will return quickly after calculating if any changes need to
|
||||
// be made to the pin to flash the LED
|
||||
void LEDStatus::LEDStatus::handle() {
|
||||
// is a pin defined?
|
||||
if (_ledPin == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// are we currently running a one-shot?
|
||||
if (_oneshotCountRemaining > 0) {
|
||||
if (_oneshotCurrentlyOn) {
|
||||
if ((_timer + _oneshotOnMs) < millis()) {
|
||||
if (_oneshotOffMs > 0) {
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
}
|
||||
_oneshotCurrentlyOn = false;
|
||||
--_oneshotCountRemaining;
|
||||
if (_oneshotCountRemaining == 0) {
|
||||
_continuousCurrentlyOn = false;
|
||||
}
|
||||
_timer += _oneshotOnMs;
|
||||
}
|
||||
} else {
|
||||
if ((_timer + _oneshotOffMs) < millis()) {
|
||||
if (_oneshotOnMs > 0) {
|
||||
digitalWrite(_ledPin, _pinState(HIGH));
|
||||
}
|
||||
_oneshotCurrentlyOn = true;
|
||||
_timer += _oneshotOffMs;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// operate using continuous
|
||||
if (_continuousCurrentlyOn) {
|
||||
if ((_timer + _continuousOnMs) < millis()) {
|
||||
if (_continuousOffMs > 0) {
|
||||
digitalWrite(_ledPin, _pinState(LOW));
|
||||
}
|
||||
_continuousCurrentlyOn = false;
|
||||
_timer += _continuousOnMs;
|
||||
}
|
||||
} else {
|
||||
if ((_timer + _continuousOffMs) < millis()) {
|
||||
if (_continuousOnMs > 0) {
|
||||
digitalWrite(_ledPin, _pinState(HIGH));
|
||||
}
|
||||
_continuousCurrentlyOn = true;
|
||||
_timer += _continuousOffMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to convert an LEDMode enum to a string
|
||||
String LEDStatus::LEDModeToString(LEDStatus::LEDMode mode) {
|
||||
switch (mode) {
|
||||
case LEDStatus::LEDMode::Off:
|
||||
return "Off";
|
||||
case LEDStatus::LEDMode::SlowToggle:
|
||||
return "Slow toggle";
|
||||
case LEDStatus::LEDMode::FastToggle:
|
||||
return "Fast toggle";
|
||||
case LEDStatus::LEDMode::SlowBlip:
|
||||
return "Slow blip";
|
||||
case LEDStatus::LEDMode::FastBlip:
|
||||
return "Fast blip";
|
||||
case LEDStatus::LEDMode::Flicker:
|
||||
return "Flicker";
|
||||
case LEDStatus::LEDMode::On:
|
||||
return "On";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to convert a string to an LEDMode enum (note, mismatch returns LedMode::Unknown)
|
||||
LEDStatus::LEDMode LEDStatus::stringToLEDMode(String mode) {
|
||||
if (mode == "Off")
|
||||
return LEDStatus::LEDMode::Off;
|
||||
if (mode == "Slow toggle")
|
||||
return LEDStatus::LEDMode::SlowToggle;
|
||||
if (mode == "Fast toggle")
|
||||
return LEDStatus::LEDMode::FastToggle;
|
||||
if (mode == "Slow blip")
|
||||
return LEDStatus::LEDMode::SlowBlip;
|
||||
if (mode == "Fast blip")
|
||||
return LEDStatus::LEDMode::FastBlip;
|
||||
if (mode == "Flicker")
|
||||
return LEDStatus::LEDMode::Flicker;
|
||||
if (mode == "On")
|
||||
return LEDStatus::LEDMode::On;
|
||||
// unable to match...
|
||||
return LEDStatus::LEDMode::Unknown;
|
||||
}
|
||||
|
||||
|
||||
// private helper converts mode to on/off times in ms
|
||||
void LEDStatus::_modeToTime(LEDStatus::LEDMode mode, uint16_t& ledOffMs, uint16_t& ledOnMs) {
|
||||
switch (mode) {
|
||||
case LEDMode::Off:
|
||||
ledOffMs = 1000;
|
||||
ledOnMs = 0;
|
||||
break;
|
||||
case LEDMode::SlowToggle:
|
||||
ledOffMs = 1000;
|
||||
ledOnMs = 1000;
|
||||
break;
|
||||
case LEDMode::FastToggle:
|
||||
ledOffMs = 100;
|
||||
ledOnMs = 100;
|
||||
break;
|
||||
case LEDMode::SlowBlip:
|
||||
ledOffMs = 1500;
|
||||
ledOnMs = 50;
|
||||
break;
|
||||
case LEDMode::FastBlip:
|
||||
ledOffMs = 333;
|
||||
ledOnMs = 50;
|
||||
break;
|
||||
case LEDMode::On:
|
||||
ledOffMs = 0;
|
||||
ledOnMs = 1000;
|
||||
break;
|
||||
case LEDMode::Flicker:
|
||||
ledOffMs = 50;
|
||||
ledOnMs = 30;
|
||||
break;
|
||||
default:
|
||||
Serial.printf_P(PSTR("LEDStatus::_modeToTime: Uknown LED mode %d\n"), mode);
|
||||
ledOffMs = 500;
|
||||
ledOnMs = 2000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// private helper to optionally inverse the LED
|
||||
uint8_t LEDStatus::_pinState(uint8_t val) {
|
||||
if (_inverse) {
|
||||
return (val == LOW) ? HIGH : LOW;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
49
lib/LEDStatus/LEDStatus.h
Normal file
49
lib/LEDStatus/LEDStatus.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef _LED_STATUS_H
|
||||
#define _LED_STATUS_H
|
||||
|
||||
class LEDStatus {
|
||||
public:
|
||||
enum class LEDMode {
|
||||
Off,
|
||||
SlowToggle,
|
||||
FastToggle,
|
||||
SlowBlip,
|
||||
FastBlip,
|
||||
Flicker,
|
||||
On,
|
||||
Unknown
|
||||
};
|
||||
LEDStatus(int8_t ledPin);
|
||||
void changePin(int8_t ledPin);
|
||||
void continuous(LEDMode mode);
|
||||
void continuous(uint16_t ledOffMs, uint16_t ledOnMs);
|
||||
void oneshot(LEDMode mode, uint8_t count = 1);
|
||||
void oneshot(uint16_t ledOffMs, uint16_t ledOnMs, uint8_t count = 1);
|
||||
|
||||
static String LEDModeToString(LEDMode mode);
|
||||
static LEDMode stringToLEDMode(String mode);
|
||||
|
||||
void handle();
|
||||
|
||||
private:
|
||||
void _modeToTime(LEDMode mode, uint16_t& ledOffMs, uint16_t& ledOnMs);
|
||||
uint8_t _pinState(uint8_t val);
|
||||
uint8_t _ledPin;
|
||||
bool _inverse;
|
||||
|
||||
uint16_t _continuousOffMs = 1000;
|
||||
uint16_t _continuousOnMs = 0;
|
||||
bool _continuousCurrentlyOn = false;
|
||||
|
||||
uint16_t _oneshotOffMs;
|
||||
uint16_t _oneshotOnMs;
|
||||
uint8_t _oneshotCountRemaining = 0;
|
||||
bool _oneshotCurrentlyOn = false;
|
||||
|
||||
unsigned long _timer = 0;
|
||||
};
|
||||
|
||||
#endif
|
59
lib/MQTT/BulbStateUpdater.cpp
Normal file
59
lib/MQTT/BulbStateUpdater.cpp
Normal file
|
@ -0,0 +1,59 @@
|
|||
#include <BulbStateUpdater.h>
|
||||
|
||||
BulbStateUpdater::BulbStateUpdater(Settings& settings, MqttClient& mqttClient, GroupStateStore& stateStore)
|
||||
: settings(settings),
|
||||
mqttClient(mqttClient),
|
||||
stateStore(stateStore),
|
||||
lastFlush(0),
|
||||
lastQueue(0),
|
||||
enabled(true)
|
||||
{ }
|
||||
|
||||
void BulbStateUpdater::enable() {
|
||||
this->enabled = true;
|
||||
}
|
||||
|
||||
void BulbStateUpdater::disable() {
|
||||
this->enabled = false;
|
||||
}
|
||||
|
||||
void BulbStateUpdater::enqueueUpdate(BulbId bulbId, GroupState& groupState) {
|
||||
staleGroups.push(bulbId);
|
||||
//Remember time, when queue was added for debounce delay
|
||||
lastQueue = millis();
|
||||
|
||||
}
|
||||
|
||||
void BulbStateUpdater::loop() {
|
||||
while (canFlush() && staleGroups.size() > 0) {
|
||||
BulbId bulbId = staleGroups.shift();
|
||||
GroupState* groupState = stateStore.get(bulbId);
|
||||
|
||||
if (groupState->isMqttDirty()) {
|
||||
flushGroup(bulbId, *groupState);
|
||||
groupState->clearMqttDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void BulbStateUpdater::flushGroup(BulbId bulbId, GroupState& state) {
|
||||
char buffer[200];
|
||||
StaticJsonDocument<200> json;
|
||||
JsonObject message = json.to<JsonObject>();
|
||||
|
||||
state.applyState(message, bulbId, settings.groupStateFields);
|
||||
serializeJson(json, buffer);
|
||||
|
||||
mqttClient.sendState(
|
||||
*MiLightRemoteConfig::fromType(bulbId.deviceType),
|
||||
bulbId.deviceId,
|
||||
bulbId.groupId,
|
||||
buffer
|
||||
);
|
||||
|
||||
lastFlush = millis();
|
||||
}
|
||||
|
||||
inline bool BulbStateUpdater::canFlush() const {
|
||||
return enabled && (millis() > (lastFlush + settings.mqttStateRateLimit) && millis() > (lastQueue + settings.mqttDebounceDelay));
|
||||
}
|
35
lib/MQTT/BulbStateUpdater.h
Normal file
35
lib/MQTT/BulbStateUpdater.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Enqueues updated bulb states and flushes them at the configured interval.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <MqttClient.h>
|
||||
#include <CircularBuffer.h>
|
||||
#include <Settings.h>
|
||||
|
||||
#ifndef BULB_STATE_UPDATER
|
||||
#define BULB_STATE_UPDATER
|
||||
|
||||
class BulbStateUpdater {
|
||||
public:
|
||||
BulbStateUpdater(Settings& settings, MqttClient& mqttClient, GroupStateStore& stateStore);
|
||||
|
||||
void enqueueUpdate(BulbId bulbId, GroupState& groupState);
|
||||
void loop();
|
||||
void enable();
|
||||
void disable();
|
||||
|
||||
private:
|
||||
Settings& settings;
|
||||
MqttClient& mqttClient;
|
||||
GroupStateStore& stateStore;
|
||||
CircularBuffer<BulbId, MILIGHT_MAX_STALE_MQTT_GROUPS> staleGroups;
|
||||
unsigned long lastFlush;
|
||||
unsigned long lastQueue;
|
||||
bool enabled;
|
||||
|
||||
inline void flushGroup(BulbId bulbId, GroupState& state);
|
||||
inline bool canFlush() const;
|
||||
};
|
||||
|
||||
#endif
|
177
lib/MQTT/HomeAssistantDiscoveryClient.cpp
Normal file
177
lib/MQTT/HomeAssistantDiscoveryClient.cpp
Normal file
|
@ -0,0 +1,177 @@
|
|||
#include <HomeAssistantDiscoveryClient.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
HomeAssistantDiscoveryClient::HomeAssistantDiscoveryClient(Settings& settings, MqttClient* mqttClient)
|
||||
: settings(settings)
|
||||
, mqttClient(mqttClient)
|
||||
{ }
|
||||
|
||||
void HomeAssistantDiscoveryClient::sendDiscoverableDevices(const std::map<String, BulbId>& aliases) {
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.println(F("HomeAssistantDiscoveryClient: Sending discoverable devices..."));
|
||||
#endif
|
||||
|
||||
for (auto itr = aliases.begin(); itr != aliases.end(); ++itr) {
|
||||
addConfig(itr->first.c_str(), itr->second);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeAssistantDiscoveryClient::removeOldDevices(const std::map<uint32_t, BulbId>& aliases) {
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.println(F("HomeAssistantDiscoveryClient: Removing discoverable devices..."));
|
||||
#endif
|
||||
|
||||
for (auto itr = aliases.begin(); itr != aliases.end(); ++itr) {
|
||||
removeConfig(itr->second);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeAssistantDiscoveryClient::removeConfig(const BulbId& bulbId) {
|
||||
// Remove by publishing an empty message
|
||||
String topic = buildTopic(bulbId);
|
||||
mqttClient->send(topic.c_str(), "", true);
|
||||
}
|
||||
|
||||
void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bulbId) {
|
||||
String topic = buildTopic(bulbId);
|
||||
DynamicJsonDocument config(1024);
|
||||
|
||||
char uniqidBuffer[30];
|
||||
sprintf_P(uniqidBuffer, PSTR("%X-%s"), ESP.getChipId(), alias);
|
||||
|
||||
config[F("schema")] = F("json");
|
||||
config[F("name")] = alias;
|
||||
config[F("command_topic")] = mqttClient->bindTopicString(settings.mqttTopicPattern, bulbId);
|
||||
config[F("state_topic")] = mqttClient->bindTopicString(settings.mqttStateTopicPattern, bulbId);
|
||||
config[F("uniq_id")] = mqttClient->bindTopicString(uniqidBuffer, bulbId);
|
||||
JsonObject deviceMetadata = config.createNestedObject(F("device"));
|
||||
|
||||
deviceMetadata[F("manufacturer")] = F("esp8266_milight_hub");
|
||||
deviceMetadata[F("sw_version")] = QUOTE(MILIGHT_HUB_VERSION);
|
||||
|
||||
JsonArray identifiers = deviceMetadata.createNestedArray(F("identifiers"));
|
||||
identifiers.add(ESP.getChipId());
|
||||
bulbId.serialize(identifiers);
|
||||
|
||||
// HomeAssistant only supports simple client availability
|
||||
if (settings.mqttClientStatusTopic.length() > 0 && settings.simpleMqttClientStatus) {
|
||||
config[F("availability_topic")] = settings.mqttClientStatusTopic;
|
||||
config[F("payload_available")] = F("connected");
|
||||
config[F("payload_not_available")] = F("disconnected");
|
||||
}
|
||||
|
||||
// Configure supported commands based on the bulb type
|
||||
|
||||
// All supported bulbs support brightness and night mode
|
||||
config[GroupStateFieldNames::BRIGHTNESS] = true;
|
||||
config[GroupStateFieldNames::EFFECT] = true;
|
||||
|
||||
JsonArray effects = config.createNestedArray(F("effect_list"));
|
||||
effects.add(MiLightCommandNames::NIGHT_MODE);
|
||||
|
||||
// These bulbs support switching between rgb/white, and have a "white_mode" command
|
||||
switch (bulbId.deviceType) {
|
||||
case REMOTE_TYPE_FUT089:
|
||||
case REMOTE_TYPE_RGB_CCT:
|
||||
case REMOTE_TYPE_RGBW:
|
||||
effects.add("white_mode");
|
||||
break;
|
||||
default:
|
||||
break; //nothing
|
||||
}
|
||||
|
||||
// All bulbs except CCT have 9 modes. FUT029 and RGB/FUT096 has 9 modes, but they
|
||||
// are not selectable directly. There are only "next mode" commands.
|
||||
switch (bulbId.deviceType) {
|
||||
case REMOTE_TYPE_CCT:
|
||||
case REMOTE_TYPE_RGB:
|
||||
case REMOTE_TYPE_FUT020:
|
||||
break;
|
||||
default:
|
||||
addNumberedEffects(effects, 0, 8);
|
||||
break;
|
||||
}
|
||||
|
||||
// These bulbs support RGB color
|
||||
switch (bulbId.deviceType) {
|
||||
case REMOTE_TYPE_FUT089:
|
||||
case REMOTE_TYPE_RGB:
|
||||
case REMOTE_TYPE_RGB_CCT:
|
||||
case REMOTE_TYPE_RGBW:
|
||||
config[F("rgb")] = true;
|
||||
break;
|
||||
default:
|
||||
break; //nothing
|
||||
}
|
||||
|
||||
// These bulbs support adjustable white values
|
||||
switch (bulbId.deviceType) {
|
||||
case REMOTE_TYPE_CCT:
|
||||
case REMOTE_TYPE_FUT089:
|
||||
case REMOTE_TYPE_FUT091:
|
||||
case REMOTE_TYPE_RGB_CCT:
|
||||
config[GroupStateFieldNames::COLOR_TEMP] = true;
|
||||
break;
|
||||
default:
|
||||
break; //nothing
|
||||
}
|
||||
|
||||
String message;
|
||||
serializeJson(config, message);
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.printf_P(PSTR("HomeAssistantDiscoveryClient: adding discoverable device: %s...\n"), alias);
|
||||
Serial.printf_P(PSTR(" topic: %s\nconfig: %s\n"), topic.c_str(), message.c_str());
|
||||
#endif
|
||||
|
||||
|
||||
mqttClient->send(topic.c_str(), message.c_str(), true);
|
||||
}
|
||||
|
||||
// Topic syntax:
|
||||
// <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
|
||||
//
|
||||
// source: https://www.home-assistant.io/docs/mqtt/discovery/
|
||||
String HomeAssistantDiscoveryClient::buildTopic(const BulbId& bulbId) {
|
||||
String topic = settings.homeAssistantDiscoveryPrefix;
|
||||
|
||||
// Don't require the user to entier a "/" (or break things if they do)
|
||||
if (! topic.endsWith("/")) {
|
||||
topic += "/";
|
||||
}
|
||||
|
||||
topic += "light/";
|
||||
// Use a static ID that doesn't depend on configuration.
|
||||
topic += "milight_hub_" + String(ESP.getChipId());
|
||||
|
||||
// make the object ID based on the actual parameters rather than the alias.
|
||||
topic += "/";
|
||||
topic += MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType);
|
||||
topic += "_";
|
||||
topic += bulbId.getHexDeviceId();
|
||||
topic += "_";
|
||||
topic += bulbId.groupId;
|
||||
topic += "/config";
|
||||
|
||||
return topic;
|
||||
}
|
||||
|
||||
String HomeAssistantDiscoveryClient::bindTopicVariables(const String& topic, const char* alias, const BulbId& bulbId) {
|
||||
String boundTopic = topic;
|
||||
String hexDeviceId = bulbId.getHexDeviceId();
|
||||
|
||||
boundTopic.replace(":device_alias", alias);
|
||||
boundTopic.replace(":device_id", hexDeviceId);
|
||||
boundTopic.replace(":hex_device_id", hexDeviceId);
|
||||
boundTopic.replace(":dec_device_id", String(bulbId.deviceId));
|
||||
boundTopic.replace(":device_type", MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType));
|
||||
boundTopic.replace(":group_id", String(bulbId.groupId));
|
||||
|
||||
return boundTopic;
|
||||
}
|
||||
|
||||
void HomeAssistantDiscoveryClient::addNumberedEffects(JsonArray& effectList, uint8_t start, uint8_t end) {
|
||||
for (uint8_t i = start; i <= end; ++i) {
|
||||
effectList.add(String(i));
|
||||
}
|
||||
}
|
24
lib/MQTT/HomeAssistantDiscoveryClient.h
Normal file
24
lib/MQTT/HomeAssistantDiscoveryClient.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <BulbId.h>
|
||||
#include <MqttClient.h>
|
||||
#include <map>
|
||||
|
||||
class HomeAssistantDiscoveryClient {
|
||||
public:
|
||||
HomeAssistantDiscoveryClient(Settings& settings, MqttClient* mqttClient);
|
||||
|
||||
void addConfig(const char* alias, const BulbId& bulbId);
|
||||
void removeConfig(const BulbId& bulbId);
|
||||
|
||||
void sendDiscoverableDevices(const std::map<String, BulbId>& aliases);
|
||||
void removeOldDevices(const std::map<uint32_t, BulbId>& aliases);
|
||||
|
||||
private:
|
||||
Settings& settings;
|
||||
MqttClient* mqttClient;
|
||||
|
||||
String buildTopic(const BulbId& bulbId);
|
||||
String bindTopicVariables(const String& topic, const char* alias, const BulbId& bulbId);
|
||||
void addNumberedEffects(JsonArray& effectList, uint8_t start, uint8_t end);
|
||||
};
|
320
lib/MQTT/MqttClient.cpp
Normal file
320
lib/MQTT/MqttClient.cpp
Normal file
|
@ -0,0 +1,320 @@
|
|||
#include <stddef.h>
|
||||
#include <MqttClient.h>
|
||||
#include <TokenIterator.h>
|
||||
#include <UrlTokenBindings.h>
|
||||
#include <IntParsing.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <AboutHelper.h>
|
||||
|
||||
static const char* STATUS_CONNECTED = "connected";
|
||||
static const char* STATUS_DISCONNECTED = "disconnected_clean";
|
||||
static const char* STATUS_LWT_DISCONNECTED = "disconnected_unclean";
|
||||
|
||||
MqttClient::MqttClient(Settings& settings, MiLightClient*& milightClient)
|
||||
: mqttClient(tcpClient),
|
||||
milightClient(milightClient),
|
||||
settings(settings),
|
||||
lastConnectAttempt(0),
|
||||
connected(false)
|
||||
{
|
||||
String strDomain = settings.mqttServer();
|
||||
this->domain = new char[strDomain.length() + 1];
|
||||
strcpy(this->domain, strDomain.c_str());
|
||||
}
|
||||
|
||||
MqttClient::~MqttClient() {
|
||||
String aboutStr = generateConnectionStatusMessage(STATUS_DISCONNECTED);
|
||||
mqttClient.publish(settings.mqttClientStatusTopic.c_str(), aboutStr.c_str(), true);
|
||||
mqttClient.disconnect();
|
||||
delete this->domain;
|
||||
}
|
||||
|
||||
void MqttClient::onConnect(OnConnectFn fn) {
|
||||
this->onConnectFn = fn;
|
||||
}
|
||||
|
||||
void MqttClient::begin() {
|
||||
#ifdef MQTT_DEBUG
|
||||
printf_P(
|
||||
PSTR("MqttClient - Connecting to: %s\nparsed:%s:%u\n"),
|
||||
settings._mqttServer.c_str(),
|
||||
settings.mqttServer().c_str(),
|
||||
settings.mqttPort()
|
||||
);
|
||||
#endif
|
||||
|
||||
mqttClient.setServer(this->domain, settings.mqttPort());
|
||||
mqttClient.setCallback(
|
||||
[this](char* topic, byte* payload, int length) {
|
||||
this->publishCallback(topic, payload, length);
|
||||
}
|
||||
);
|
||||
reconnect();
|
||||
}
|
||||
|
||||
bool MqttClient::connect() {
|
||||
char nameBuffer[30];
|
||||
sprintf_P(nameBuffer, PSTR("milight-hub-%u"), ESP.getChipId());
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.println(F("MqttClient - connecting using name"));
|
||||
Serial.println(nameBuffer);
|
||||
#endif
|
||||
|
||||
if (settings.mqttUsername.length() > 0 && settings.mqttClientStatusTopic.length() > 0) {
|
||||
return mqttClient.connect(
|
||||
nameBuffer,
|
||||
settings.mqttUsername.c_str(),
|
||||
settings.mqttPassword.c_str(),
|
||||
settings.mqttClientStatusTopic.c_str(),
|
||||
2,
|
||||
true,
|
||||
generateConnectionStatusMessage(STATUS_LWT_DISCONNECTED).c_str()
|
||||
);
|
||||
} else if (settings.mqttUsername.length() > 0) {
|
||||
return mqttClient.connect(
|
||||
nameBuffer,
|
||||
settings.mqttUsername.c_str(),
|
||||
settings.mqttPassword.c_str()
|
||||
);
|
||||
} else if (settings.mqttClientStatusTopic.length() > 0) {
|
||||
return mqttClient.connect(
|
||||
nameBuffer,
|
||||
settings.mqttClientStatusTopic.c_str(),
|
||||
2,
|
||||
true,
|
||||
generateConnectionStatusMessage(STATUS_LWT_DISCONNECTED).c_str()
|
||||
);
|
||||
} else {
|
||||
return mqttClient.connect(nameBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
void MqttClient::sendBirthMessage() {
|
||||
if (settings.mqttClientStatusTopic.length() > 0) {
|
||||
String aboutStr = generateConnectionStatusMessage(STATUS_CONNECTED);
|
||||
mqttClient.publish(settings.mqttClientStatusTopic.c_str(), aboutStr.c_str(), true);
|
||||
}
|
||||
}
|
||||
|
||||
void MqttClient::reconnect() {
|
||||
if (lastConnectAttempt > 0 && (millis() - lastConnectAttempt) < MQTT_CONNECTION_ATTEMPT_FREQUENCY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! mqttClient.connected()) {
|
||||
if (connect()) {
|
||||
subscribe();
|
||||
sendBirthMessage();
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.println(F("MqttClient - Successfully connected to MQTT server"));
|
||||
#endif
|
||||
} else {
|
||||
Serial.print(F("ERROR: Failed to connect to MQTT server rc="));
|
||||
Serial.println(mqttClient.state());
|
||||
}
|
||||
}
|
||||
|
||||
lastConnectAttempt = millis();
|
||||
}
|
||||
|
||||
void MqttClient::handleClient() {
|
||||
reconnect();
|
||||
mqttClient.loop();
|
||||
|
||||
if (!connected && mqttClient.connected()) {
|
||||
this->connected = true;
|
||||
this->onConnectFn();
|
||||
} else if (!mqttClient.connected()) {
|
||||
this->connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MqttClient::sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update) {
|
||||
publish(settings.mqttUpdateTopicPattern, remoteConfig, deviceId, groupId, update, false);
|
||||
}
|
||||
|
||||
void MqttClient::sendState(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update) {
|
||||
publish(settings.mqttStateTopicPattern, remoteConfig, deviceId, groupId, update, true);
|
||||
}
|
||||
|
||||
void MqttClient::subscribe() {
|
||||
String topic = settings.mqttTopicPattern;
|
||||
|
||||
topic.replace(":device_id", "+");
|
||||
topic.replace(":hex_device_id", "+");
|
||||
topic.replace(":dec_device_id", "+");
|
||||
topic.replace(":group_id", "+");
|
||||
topic.replace(":device_type", "+");
|
||||
topic.replace(":device_alias", "+");
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
printf_P(PSTR("MqttClient - subscribing to topic: %s\n"), topic.c_str());
|
||||
#endif
|
||||
|
||||
mqttClient.subscribe(topic.c_str());
|
||||
}
|
||||
|
||||
void MqttClient::send(const char* topic, const char* message, const bool retain) {
|
||||
size_t len = strlen(message);
|
||||
size_t topicLen = strlen(topic);
|
||||
|
||||
if ((topicLen + len + 10) < MQTT_MAX_PACKET_SIZE ) {
|
||||
mqttClient.publish(topic, message, retain);
|
||||
} else {
|
||||
const uint8_t* messageBuffer = reinterpret_cast<const uint8_t*>(message);
|
||||
mqttClient.beginPublish(topic, len, retain);
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.printf_P(PSTR("Printing message in parts because it's too large for the packet buffer (%d bytes)"), len);
|
||||
#endif
|
||||
|
||||
for (size_t i = 0; i < len; i += MQTT_PACKET_CHUNK_SIZE) {
|
||||
size_t toWrite = std::min(static_cast<size_t>(MQTT_PACKET_CHUNK_SIZE), len - i);
|
||||
mqttClient.write(messageBuffer+i, toWrite);
|
||||
#ifdef MQTT_DEBUG
|
||||
Serial.printf_P(PSTR(" Wrote %d bytes\n"), toWrite);
|
||||
#endif
|
||||
}
|
||||
|
||||
mqttClient.endPublish();
|
||||
}
|
||||
}
|
||||
|
||||
void MqttClient::publish(
|
||||
const String& _topic,
|
||||
const MiLightRemoteConfig &remoteConfig,
|
||||
uint16_t deviceId,
|
||||
uint16_t groupId,
|
||||
const char* message,
|
||||
const bool _retain
|
||||
) {
|
||||
if (_topic.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BulbId bulbId(deviceId, groupId, remoteConfig.type);
|
||||
String topic = bindTopicString(_topic, bulbId);
|
||||
const bool retain = _retain && this->settings.mqttRetain;
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
printf("MqttClient - publishing update to %s\n", topic.c_str());
|
||||
#endif
|
||||
|
||||
send(topic.c_str(), message, retain);
|
||||
}
|
||||
|
||||
void MqttClient::publishCallback(char* topic, byte* payload, int length) {
|
||||
uint16_t deviceId = 0;
|
||||
uint8_t groupId = 0;
|
||||
const MiLightRemoteConfig* config = &FUT092Config;
|
||||
char cstrPayload[length + 1];
|
||||
cstrPayload[length] = 0;
|
||||
memcpy(cstrPayload, payload, sizeof(byte)*length);
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
printf("MqttClient - Got message on topic: %s\n%s\n", topic, cstrPayload);
|
||||
#endif
|
||||
|
||||
char topicPattern[settings.mqttTopicPattern.length()];
|
||||
strcpy(topicPattern, settings.mqttTopicPattern.c_str());
|
||||
|
||||
TokenIterator patternIterator(topicPattern, settings.mqttTopicPattern.length(), '/');
|
||||
TokenIterator topicIterator(topic, strlen(topic), '/');
|
||||
UrlTokenBindings tokenBindings(patternIterator, topicIterator);
|
||||
|
||||
if (tokenBindings.hasBinding("device_alias")) {
|
||||
String alias = tokenBindings.get("device_alias");
|
||||
auto itr = settings.groupIdAliases.find(alias);
|
||||
|
||||
if (itr == settings.groupIdAliases.end()) {
|
||||
Serial.printf_P(PSTR("MqttClient - WARNING: could not find device alias: `%s'. Ignoring packet.\n"), alias.c_str());
|
||||
return;
|
||||
} else {
|
||||
BulbId bulbId = itr->second;
|
||||
|
||||
deviceId = bulbId.deviceId;
|
||||
config = MiLightRemoteConfig::fromType(bulbId.deviceType);
|
||||
groupId = bulbId.groupId;
|
||||
}
|
||||
} else {
|
||||
if (tokenBindings.hasBinding(GroupStateFieldNames::DEVICE_ID)) {
|
||||
deviceId = parseInt<uint16_t>(tokenBindings.get(GroupStateFieldNames::DEVICE_ID));
|
||||
} else if (tokenBindings.hasBinding("hex_device_id")) {
|
||||
deviceId = parseInt<uint16_t>(tokenBindings.get("hex_device_id"));
|
||||
} else if (tokenBindings.hasBinding("dec_device_id")) {
|
||||
deviceId = parseInt<uint16_t>(tokenBindings.get("dec_device_id"));
|
||||
}
|
||||
|
||||
if (tokenBindings.hasBinding(GroupStateFieldNames::GROUP_ID)) {
|
||||
groupId = parseInt<uint16_t>(tokenBindings.get(GroupStateFieldNames::GROUP_ID));
|
||||
}
|
||||
|
||||
if (tokenBindings.hasBinding(GroupStateFieldNames::DEVICE_TYPE)) {
|
||||
config = MiLightRemoteConfig::fromType(tokenBindings.get(GroupStateFieldNames::DEVICE_TYPE));
|
||||
} else {
|
||||
Serial.println(F("MqttClient - WARNING: could not find device_type token. Defaulting to FUT092.\n"));
|
||||
}
|
||||
}
|
||||
|
||||
if (config == NULL) {
|
||||
Serial.println(F("MqttClient - ERROR: unknown device_type specified"));
|
||||
return;
|
||||
}
|
||||
|
||||
StaticJsonDocument<400> buffer;
|
||||
deserializeJson(buffer, cstrPayload);
|
||||
JsonObject obj = buffer.as<JsonObject>();
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
printf("MqttClient - device %04X, group %u\n", deviceId, groupId);
|
||||
#endif
|
||||
|
||||
milightClient->prepare(config, deviceId, groupId);
|
||||
milightClient->update(obj);
|
||||
}
|
||||
|
||||
String MqttClient::bindTopicString(const String& topicPattern, const BulbId& bulbId) {
|
||||
String boundTopic = topicPattern;
|
||||
String deviceIdHex = bulbId.getHexDeviceId();
|
||||
|
||||
boundTopic.replace(":device_id", deviceIdHex);
|
||||
boundTopic.replace(":hex_device_id", deviceIdHex);
|
||||
boundTopic.replace(":dec_device_id", String(bulbId.deviceId));
|
||||
boundTopic.replace(":group_id", String(bulbId.groupId));
|
||||
boundTopic.replace(":device_type", MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType));
|
||||
|
||||
auto it = settings.findAlias(bulbId.deviceType, bulbId.deviceId, bulbId.groupId);
|
||||
if (it != settings.groupIdAliases.end()) {
|
||||
boundTopic.replace(":device_alias", it->first);
|
||||
} else {
|
||||
boundTopic.replace(":device_alias", "__unnamed_group");
|
||||
}
|
||||
|
||||
return boundTopic;
|
||||
}
|
||||
|
||||
String MqttClient::generateConnectionStatusMessage(const char* connectionStatus) {
|
||||
if (settings.simpleMqttClientStatus) {
|
||||
// Don't expand disconnect type for simple status
|
||||
if (0 == strcmp(connectionStatus, STATUS_CONNECTED)) {
|
||||
return connectionStatus;
|
||||
} else {
|
||||
return "disconnected";
|
||||
}
|
||||
} else {
|
||||
StaticJsonDocument<1024> json;
|
||||
json[GroupStateFieldNames::STATUS] = connectionStatus;
|
||||
|
||||
// Fill other fields
|
||||
AboutHelper::generateAboutObject(json, true);
|
||||
|
||||
String response;
|
||||
serializeJson(json, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
61
lib/MQTT/MqttClient.h
Normal file
61
lib/MQTT/MqttClient.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
#include <MiLightClient.h>
|
||||
#include <Settings.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
|
||||
#ifndef MQTT_CONNECTION_ATTEMPT_FREQUENCY
|
||||
#define MQTT_CONNECTION_ATTEMPT_FREQUENCY 5000
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_PACKET_CHUNK_SIZE
|
||||
#define MQTT_PACKET_CHUNK_SIZE 128
|
||||
#endif
|
||||
|
||||
#ifndef _MQTT_CLIENT_H
|
||||
#define _MQTT_CLIENT_H
|
||||
|
||||
class MqttClient {
|
||||
public:
|
||||
using OnConnectFn = std::function<void()>;
|
||||
|
||||
MqttClient(Settings& settings, MiLightClient*& milightClient);
|
||||
~MqttClient();
|
||||
|
||||
void begin();
|
||||
void handleClient();
|
||||
void reconnect();
|
||||
void sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update);
|
||||
void sendState(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update);
|
||||
void send(const char* topic, const char* message, const bool retain = false);
|
||||
void onConnect(OnConnectFn fn);
|
||||
|
||||
String bindTopicString(const String& topicPattern, const BulbId& bulbId);
|
||||
|
||||
private:
|
||||
WiFiClient tcpClient;
|
||||
PubSubClient mqttClient;
|
||||
MiLightClient*& milightClient;
|
||||
Settings& settings;
|
||||
char* domain;
|
||||
unsigned long lastConnectAttempt;
|
||||
OnConnectFn onConnectFn;
|
||||
bool connected;
|
||||
|
||||
void sendBirthMessage();
|
||||
bool connect();
|
||||
void subscribe();
|
||||
void publishCallback(char* topic, byte* payload, int length);
|
||||
void publish(
|
||||
const String& topic,
|
||||
const MiLightRemoteConfig& remoteConfig,
|
||||
uint16_t deviceId,
|
||||
uint16_t groupId,
|
||||
const char* update,
|
||||
const bool retain = false
|
||||
);
|
||||
|
||||
String generateConnectionStatusMessage(const char* status);
|
||||
};
|
||||
|
||||
#endif
|
225
lib/MiLight/CctPacketFormatter.cpp
Normal file
225
lib/MiLight/CctPacketFormatter.cpp
Normal file
|
@ -0,0 +1,225 @@
|
|||
#include <CctPacketFormatter.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
static const uint8_t CCT_PROTOCOL_ID = 0x5A;
|
||||
|
||||
bool CctPacketFormatter::canHandle(const uint8_t *packet, const size_t len) {
|
||||
return len == packetLength && packet[0] == CCT_PROTOCOL_ID;
|
||||
}
|
||||
|
||||
void CctPacketFormatter::initializePacket(uint8_t* packet) {
|
||||
size_t packetPtr = 0;
|
||||
|
||||
// Byte 0: Packet length = 7 bytes
|
||||
|
||||
// Byte 1: CCT protocol
|
||||
packet[packetPtr++] = CCT_PROTOCOL_ID;
|
||||
|
||||
// Byte 2 and 3: Device ID
|
||||
packet[packetPtr++] = deviceId >> 8;
|
||||
packet[packetPtr++] = deviceId & 0xFF;
|
||||
|
||||
// Byte 4: Zone
|
||||
packet[packetPtr++] = groupId;
|
||||
|
||||
// Byte 5: Bulb command, filled in later
|
||||
packet[packetPtr++] = 0;
|
||||
|
||||
// Byte 6: Packet sequence number 0..255
|
||||
packet[packetPtr++] = sequenceNum++;
|
||||
|
||||
// Byte 7: Checksum over previous bytes, including packet length = 7
|
||||
// The checksum will be calculated when setting the command field
|
||||
packet[packetPtr++] = 0;
|
||||
|
||||
// Byte 8: CRC LSB
|
||||
// Byte 9: CRC MSB
|
||||
}
|
||||
|
||||
void CctPacketFormatter::finalizePacket(uint8_t* packet) {
|
||||
uint8_t checksum;
|
||||
|
||||
// Calculate checksum over packet length .. sequenceNum
|
||||
checksum = 7; // Packet length is not part of packet
|
||||
for (uint8_t i = 0; i < 6; i++) {
|
||||
checksum += currentPacket[i];
|
||||
}
|
||||
// Store the checksum in the sixth byte
|
||||
currentPacket[6] = checksum;
|
||||
}
|
||||
|
||||
void CctPacketFormatter::updateBrightness(uint8_t value) {
|
||||
const GroupState* state = this->stateStore->get(deviceId, groupId, MiLightRemoteType::REMOTE_TYPE_CCT);
|
||||
int8_t knownValue = (state != NULL && state->isSetBrightness()) ? state->getBrightness() / CCT_INTERVALS : -1;
|
||||
|
||||
valueByStepFunction(
|
||||
&PacketFormatter::increaseBrightness,
|
||||
&PacketFormatter::decreaseBrightness,
|
||||
CCT_INTERVALS,
|
||||
value / CCT_INTERVALS,
|
||||
knownValue
|
||||
);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::updateTemperature(uint8_t value) {
|
||||
const GroupState* state = this->stateStore->get(deviceId, groupId, MiLightRemoteType::REMOTE_TYPE_CCT);
|
||||
int8_t knownValue = (state != NULL && state->isSetKelvin()) ? state->getKelvin() / CCT_INTERVALS : -1;
|
||||
|
||||
valueByStepFunction(
|
||||
&PacketFormatter::increaseTemperature,
|
||||
&PacketFormatter::decreaseTemperature,
|
||||
CCT_INTERVALS,
|
||||
value / CCT_INTERVALS,
|
||||
knownValue
|
||||
);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::command(uint8_t command, uint8_t arg) {
|
||||
pushPacket();
|
||||
if (held) {
|
||||
command |= 0x80;
|
||||
}
|
||||
currentPacket[CCT_COMMAND_INDEX] = command;
|
||||
}
|
||||
|
||||
void CctPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
|
||||
command(getCctStatusButton(groupId, status), 0);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::increaseTemperature() {
|
||||
command(CCT_TEMPERATURE_UP, 0);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::decreaseTemperature() {
|
||||
command(CCT_TEMPERATURE_DOWN, 0);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::increaseBrightness() {
|
||||
command(CCT_BRIGHTNESS_UP, 0);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::decreaseBrightness() {
|
||||
command(CCT_BRIGHTNESS_DOWN, 0);
|
||||
}
|
||||
|
||||
void CctPacketFormatter::enableNightMode() {
|
||||
command(getCctStatusButton(groupId, OFF) | 0x10, 0);
|
||||
}
|
||||
|
||||
uint8_t CctPacketFormatter::getCctStatusButton(uint8_t groupId, MiLightStatus status) {
|
||||
uint8_t button = 0;
|
||||
|
||||
if (status == ON) {
|
||||
switch(groupId) {
|
||||
case 0:
|
||||
button = CCT_ALL_ON;
|
||||
break;
|
||||
case 1:
|
||||
button = CCT_GROUP_1_ON;
|
||||
break;
|
||||
case 2:
|
||||
button = CCT_GROUP_2_ON;
|
||||
break;
|
||||
case 3:
|
||||
button = CCT_GROUP_3_ON;
|
||||
break;
|
||||
case 4:
|
||||
button = CCT_GROUP_4_ON;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch(groupId) {
|
||||
case 0:
|
||||
button = CCT_ALL_OFF;
|
||||
break;
|
||||
case 1:
|
||||
button = CCT_GROUP_1_OFF;
|
||||
break;
|
||||
case 2:
|
||||
button = CCT_GROUP_2_OFF;
|
||||
break;
|
||||
case 3:
|
||||
button = CCT_GROUP_3_OFF;
|
||||
break;
|
||||
case 4:
|
||||
button = CCT_GROUP_4_OFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
uint8_t CctPacketFormatter::cctCommandIdToGroup(uint8_t command) {
|
||||
switch (command & 0xF) {
|
||||
case CCT_GROUP_1_ON:
|
||||
case CCT_GROUP_1_OFF:
|
||||
return 1;
|
||||
case CCT_GROUP_2_ON:
|
||||
case CCT_GROUP_2_OFF:
|
||||
return 2;
|
||||
case CCT_GROUP_3_ON:
|
||||
case CCT_GROUP_3_OFF:
|
||||
return 3;
|
||||
case CCT_GROUP_4_ON:
|
||||
case CCT_GROUP_4_OFF:
|
||||
return 4;
|
||||
case CCT_ALL_ON:
|
||||
case CCT_ALL_OFF:
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 255;
|
||||
}
|
||||
|
||||
MiLightStatus CctPacketFormatter::cctCommandToStatus(uint8_t command) {
|
||||
switch (command & 0xF) {
|
||||
case CCT_GROUP_1_ON:
|
||||
case CCT_GROUP_2_ON:
|
||||
case CCT_GROUP_3_ON:
|
||||
case CCT_GROUP_4_ON:
|
||||
case CCT_ALL_ON:
|
||||
return ON;
|
||||
case CCT_GROUP_1_OFF:
|
||||
case CCT_GROUP_2_OFF:
|
||||
case CCT_GROUP_3_OFF:
|
||||
case CCT_GROUP_4_OFF:
|
||||
case CCT_ALL_OFF:
|
||||
default:
|
||||
return OFF;
|
||||
}
|
||||
}
|
||||
|
||||
BulbId CctPacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
|
||||
uint8_t command = packet[CCT_COMMAND_INDEX] & 0x7F;
|
||||
|
||||
uint8_t onOffGroupId = cctCommandIdToGroup(command);
|
||||
BulbId bulbId(
|
||||
(packet[1] << 8) | packet[2],
|
||||
onOffGroupId < 255 ? onOffGroupId : packet[3],
|
||||
REMOTE_TYPE_CCT
|
||||
);
|
||||
|
||||
// Night mode
|
||||
if (command & 0x10) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::NIGHT_MODE;
|
||||
} else if (onOffGroupId < 255) {
|
||||
result[GroupStateFieldNames::STATE] = cctCommandToStatus(command) == ON ? "ON" : "OFF";
|
||||
} else if (command == CCT_BRIGHTNESS_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = "brightness_down";
|
||||
} else if (command == CCT_BRIGHTNESS_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = "brightness_up";
|
||||
} else if (command == CCT_TEMPERATURE_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::TEMPERATURE_DOWN;
|
||||
} else if (command == CCT_TEMPERATURE_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::TEMPERATURE_UP;
|
||||
} else {
|
||||
result["button_id"] = command;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
||||
|
||||
void CctPacketFormatter::format(uint8_t const* packet, char* buffer) {
|
||||
PacketFormatter::formatV1Packet(packet, buffer);
|
||||
}
|
56
lib/MiLight/CctPacketFormatter.h
Normal file
56
lib/MiLight/CctPacketFormatter.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
#include <PacketFormatter.h>
|
||||
|
||||
#ifndef _CCT_PACKET_FORMATTER_H
|
||||
#define _CCT_PACKET_FORMATTER_H
|
||||
|
||||
#define CCT_COMMAND_INDEX 4
|
||||
#define CCT_INTERVALS 10
|
||||
|
||||
enum MiLightCctButton {
|
||||
CCT_ALL_ON = 0x05,
|
||||
CCT_ALL_OFF = 0x09,
|
||||
CCT_GROUP_1_ON = 0x08,
|
||||
CCT_GROUP_1_OFF = 0x0B,
|
||||
CCT_GROUP_2_ON = 0x0D,
|
||||
CCT_GROUP_2_OFF = 0x03,
|
||||
CCT_GROUP_3_ON = 0x07,
|
||||
CCT_GROUP_3_OFF = 0x0A,
|
||||
CCT_GROUP_4_ON = 0x02,
|
||||
CCT_GROUP_4_OFF = 0x06,
|
||||
CCT_BRIGHTNESS_DOWN = 0x04,
|
||||
CCT_BRIGHTNESS_UP = 0x0C,
|
||||
CCT_TEMPERATURE_UP = 0x0E,
|
||||
CCT_TEMPERATURE_DOWN = 0x0F
|
||||
};
|
||||
|
||||
class CctPacketFormatter : public PacketFormatter {
|
||||
public:
|
||||
CctPacketFormatter()
|
||||
: PacketFormatter(REMOTE_TYPE_CCT, 7, 20)
|
||||
{ }
|
||||
|
||||
virtual bool canHandle(const uint8_t* packet, const size_t len);
|
||||
|
||||
virtual void updateStatus(MiLightStatus status, uint8_t groupId);
|
||||
virtual void command(uint8_t command, uint8_t arg);
|
||||
|
||||
virtual void updateTemperature(uint8_t value);
|
||||
virtual void increaseTemperature();
|
||||
virtual void decreaseTemperature();
|
||||
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void increaseBrightness();
|
||||
virtual void decreaseBrightness();
|
||||
virtual void enableNightMode();
|
||||
|
||||
virtual void format(uint8_t const* packet, char* buffer);
|
||||
virtual void initializePacket(uint8_t* packet);
|
||||
virtual void finalizePacket(uint8_t* packet);
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
|
||||
static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
|
||||
static uint8_t cctCommandIdToGroup(uint8_t command);
|
||||
static MiLightStatus cctCommandToStatus(uint8_t command);
|
||||
};
|
||||
|
||||
#endif
|
88
lib/MiLight/FUT020PacketFormatter.cpp
Normal file
88
lib/MiLight/FUT020PacketFormatter.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
#include <FUT020PacketFormatter.h>
|
||||
#include <Units.h>
|
||||
|
||||
void FUT020PacketFormatter::updateColorRaw(uint8_t color) {
|
||||
command(static_cast<uint8_t>(FUT020Command::COLOR), color);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::updateHue(uint16_t hue) {
|
||||
uint16_t remapped = Units::rescale<uint16_t, uint16_t>(hue, 255.0, 360.0);
|
||||
remapped = (remapped + 0xB0) % 0x100;
|
||||
|
||||
updateColorRaw(remapped);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::updateColorWhite() {
|
||||
command(static_cast<uint8_t>(FUT020Command::COLOR_WHITE_TOGGLE), 0);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::nextMode() {
|
||||
command(static_cast<uint8_t>(FUT020Command::MODE_SWITCH), 0);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::updateBrightness(uint8_t value) {
|
||||
const GroupState* state = this->stateStore->get(deviceId, groupId, MiLightRemoteType::REMOTE_TYPE_FUT020);
|
||||
int8_t knownValue = (state != NULL && state->isSetBrightness())
|
||||
? state->getBrightness() / FUT02xPacketFormatter::NUM_BRIGHTNESS_INTERVALS
|
||||
: -1;
|
||||
|
||||
valueByStepFunction(
|
||||
&PacketFormatter::increaseBrightness,
|
||||
&PacketFormatter::decreaseBrightness,
|
||||
FUT02xPacketFormatter::NUM_BRIGHTNESS_INTERVALS,
|
||||
value / FUT02xPacketFormatter::NUM_BRIGHTNESS_INTERVALS,
|
||||
knownValue
|
||||
);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::increaseBrightness() {
|
||||
command(static_cast<uint8_t>(FUT020Command::BRIGHTNESS_UP), 0);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::decreaseBrightness() {
|
||||
command(static_cast<uint8_t>(FUT020Command::BRIGHTNESS_DOWN), 0);
|
||||
}
|
||||
|
||||
void FUT020PacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
|
||||
command(static_cast<uint8_t>(FUT020Command::ON_OFF), 0);
|
||||
}
|
||||
|
||||
BulbId FUT020PacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
|
||||
FUT020Command command = static_cast<FUT020Command>(packet[FUT02xPacketFormatter::FUT02X_COMMAND_INDEX] & 0x0F);
|
||||
|
||||
BulbId bulbId(
|
||||
(packet[1] << 8) | packet[2],
|
||||
0,
|
||||
REMOTE_TYPE_FUT020
|
||||
);
|
||||
|
||||
switch (command) {
|
||||
case FUT020Command::ON_OFF:
|
||||
result[F("state")] = F("ON");
|
||||
break;
|
||||
|
||||
case FUT020Command::BRIGHTNESS_DOWN:
|
||||
result[F("command")] = F("brightness_down");
|
||||
break;
|
||||
|
||||
case FUT020Command::BRIGHTNESS_UP:
|
||||
result[F("command")] = F("brightness_up");
|
||||
break;
|
||||
|
||||
case FUT020Command::MODE_SWITCH:
|
||||
result[F("command")] = F("next_mode");
|
||||
break;
|
||||
|
||||
case FUT020Command::COLOR_WHITE_TOGGLE:
|
||||
result[F("command")] = F("color_white_toggle");
|
||||
break;
|
||||
|
||||
case FUT020Command::COLOR:
|
||||
uint16_t remappedColor = Units::rescale<uint16_t, uint16_t>(packet[FUT02xPacketFormatter::FUT02X_ARGUMENT_INDEX], 360.0, 255.0);
|
||||
remappedColor = (remappedColor + 113) % 360;
|
||||
result[GroupStateFieldNames::HUE] = remappedColor;
|
||||
break;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
30
lib/MiLight/FUT020PacketFormatter.h
Normal file
30
lib/MiLight/FUT020PacketFormatter.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
#include <FUT02xPacketFormatter.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
enum class FUT020Command {
|
||||
ON_OFF = 0x04,
|
||||
MODE_SWITCH = 0x02,
|
||||
COLOR_WHITE_TOGGLE = 0x05,
|
||||
BRIGHTNESS_DOWN = 0x01,
|
||||
BRIGHTNESS_UP = 0x03,
|
||||
COLOR = 0x00
|
||||
};
|
||||
|
||||
class FUT020PacketFormatter : public FUT02xPacketFormatter {
|
||||
public:
|
||||
FUT020PacketFormatter()
|
||||
: FUT02xPacketFormatter(REMOTE_TYPE_FUT020)
|
||||
{ }
|
||||
|
||||
virtual void updateStatus(MiLightStatus status, uint8_t groupId);
|
||||
virtual void updateHue(uint16_t value);
|
||||
virtual void updateColorRaw(uint8_t value);
|
||||
virtual void updateColorWhite();
|
||||
virtual void nextMode();
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void increaseBrightness();
|
||||
virtual void decreaseBrightness();
|
||||
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result) override;
|
||||
};
|
50
lib/MiLight/FUT02xPacketFormatter.cpp
Normal file
50
lib/MiLight/FUT02xPacketFormatter.cpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#include <FUT02xPacketFormatter.h>
|
||||
|
||||
static const uint8_t FUT02X_PACKET_HEADER = 0xA5;
|
||||
|
||||
static const uint8_t FUT02X_PAIR_COMMAND = 0x03;
|
||||
static const uint8_t FUT02X_UNPAIR_COMMAND = 0x03;
|
||||
|
||||
void FUT02xPacketFormatter::initializePacket(uint8_t *packet) {
|
||||
size_t packetPtr = 0;
|
||||
|
||||
packet[packetPtr++] = 0xA5;
|
||||
packet[packetPtr++] = deviceId >> 8;
|
||||
packet[packetPtr++] = deviceId & 0xFF;
|
||||
packet[packetPtr++] = 0; // arg
|
||||
packet[packetPtr++] = 0; // command
|
||||
packet[packetPtr++] = sequenceNum++;
|
||||
}
|
||||
|
||||
bool FUT02xPacketFormatter::canHandle(const uint8_t* packet, const size_t len) {
|
||||
return len == packetLength && packet[0] == FUT02X_PACKET_HEADER;
|
||||
}
|
||||
|
||||
void FUT02xPacketFormatter::command(uint8_t command, uint8_t arg) {
|
||||
pushPacket();
|
||||
if (held) {
|
||||
command |= 0x10;
|
||||
}
|
||||
currentPacket[FUT02X_COMMAND_INDEX] = command;
|
||||
currentPacket[FUT02X_ARGUMENT_INDEX] = arg;
|
||||
}
|
||||
|
||||
void FUT02xPacketFormatter::pair() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
command(FUT02X_PAIR_COMMAND, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void FUT02xPacketFormatter::unpair() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
command(FUT02X_PAIR_COMMAND, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void FUT02xPacketFormatter::format(uint8_t const* packet, char* buffer) {
|
||||
buffer += sprintf_P(buffer, PSTR("b0 : %02X\n"), packet[0]);
|
||||
buffer += sprintf_P(buffer, PSTR("ID : %02X%02X\n"), packet[1], packet[2]);
|
||||
buffer += sprintf_P(buffer, PSTR("Arg : %02X\n"), packet[3]);
|
||||
buffer += sprintf_P(buffer, PSTR("Command : %02X\n"), packet[4]);
|
||||
buffer += sprintf_P(buffer, PSTR("Sequence : %02X\n"), packet[5]);
|
||||
}
|
24
lib/MiLight/FUT02xPacketFormatter.h
Normal file
24
lib/MiLight/FUT02xPacketFormatter.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
#include <PacketFormatter.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
class FUT02xPacketFormatter : public PacketFormatter {
|
||||
public:
|
||||
static const uint8_t FUT02X_COMMAND_INDEX = 4;
|
||||
static const uint8_t FUT02X_ARGUMENT_INDEX = 3;
|
||||
static const uint8_t NUM_BRIGHTNESS_INTERVALS = 10;
|
||||
|
||||
FUT02xPacketFormatter(MiLightRemoteType type)
|
||||
: PacketFormatter(type, 6, 10)
|
||||
{ }
|
||||
|
||||
virtual bool canHandle(const uint8_t* packet, const size_t len) override;
|
||||
|
||||
virtual void command(uint8_t command, uint8_t arg) override;
|
||||
|
||||
virtual void pair() override;
|
||||
virtual void unpair() override;
|
||||
|
||||
virtual void initializePacket(uint8_t* packet) override;
|
||||
virtual void format(uint8_t const* packet, char* buffer) override;
|
||||
};
|
156
lib/MiLight/FUT089PacketFormatter.cpp
Normal file
156
lib/MiLight/FUT089PacketFormatter.cpp
Normal file
|
@ -0,0 +1,156 @@
|
|||
#include <FUT089PacketFormatter.h>
|
||||
#include <V2RFEncoding.h>
|
||||
#include <Units.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
void FUT089PacketFormatter::modeSpeedDown() {
|
||||
command(FUT089_ON, FUT089_MODE_SPEED_DOWN);
|
||||
}
|
||||
|
||||
void FUT089PacketFormatter::modeSpeedUp() {
|
||||
command(FUT089_ON, FUT089_MODE_SPEED_UP);
|
||||
}
|
||||
|
||||
void FUT089PacketFormatter::updateMode(uint8_t mode) {
|
||||
command(FUT089_MODE, mode);
|
||||
}
|
||||
|
||||
void FUT089PacketFormatter::updateBrightness(uint8_t brightness) {
|
||||
command(FUT089_BRIGHTNESS, brightness);
|
||||
}
|
||||
|
||||
// change the hue (which may also change to color mode).
|
||||
void FUT089PacketFormatter::updateHue(uint16_t value) {
|
||||
uint8_t remapped = Units::rescale(value, 255, 360);
|
||||
updateColorRaw(remapped);
|
||||
}
|
||||
|
||||
void FUT089PacketFormatter::updateColorRaw(uint8_t value) {
|
||||
command(FUT089_COLOR, FUT089_COLOR_OFFSET + value);
|
||||
}
|
||||
|
||||
// change the temperature (kelvin). Note that temperature and saturation share the same command
|
||||
// number (7), and they change which they do based on the mode of the lamp (white vs. color mode).
|
||||
// To make this command work, we need to switch to white mode, make the change, and then flip
|
||||
// back to the original mode.
|
||||
void FUT089PacketFormatter::updateTemperature(uint8_t value) {
|
||||
// look up our current mode
|
||||
const GroupState* ourState = this->stateStore->get(this->deviceId, this->groupId, REMOTE_TYPE_FUT089);
|
||||
BulbMode originalBulbMode;
|
||||
|
||||
if (ourState != NULL) {
|
||||
originalBulbMode = ourState->getBulbMode();
|
||||
|
||||
// are we already in white? If not, change to white
|
||||
if (originalBulbMode != BulbMode::BULB_MODE_WHITE) {
|
||||
updateColorWhite();
|
||||
}
|
||||
}
|
||||
|
||||
// now make the temperature change
|
||||
command(FUT089_KELVIN, 100 - value);
|
||||
|
||||
// and return to our original mode
|
||||
if (ourState != NULL && (settings->enableAutomaticModeSwitching) && (originalBulbMode != BulbMode::BULB_MODE_WHITE)) {
|
||||
switchMode(*ourState, originalBulbMode);
|
||||
}
|
||||
}
|
||||
|
||||
// change the saturation. Note that temperature and saturation share the same command
|
||||
// number (7), and they change which they do based on the mode of the lamp (white vs. color mode).
|
||||
// Therefore, if we are not in color mode, we need to switch to color mode, make the change,
|
||||
// and switch back to the original mode.
|
||||
void FUT089PacketFormatter::updateSaturation(uint8_t value) {
|
||||
// look up our current mode
|
||||
const GroupState* ourState = this->stateStore->get(this->deviceId, this->groupId, REMOTE_TYPE_FUT089);
|
||||
BulbMode originalBulbMode = BulbMode::BULB_MODE_WHITE;
|
||||
|
||||
if (ourState != NULL) {
|
||||
originalBulbMode = ourState->getBulbMode();
|
||||
}
|
||||
|
||||
// are we already in color? If not, we need to flip modes
|
||||
if (ourState != NULL && (settings->enableAutomaticModeSwitching) && (originalBulbMode != BulbMode::BULB_MODE_COLOR)) {
|
||||
updateHue(ourState->getHue());
|
||||
}
|
||||
|
||||
// now make the saturation change
|
||||
command(FUT089_SATURATION, 100 - value);
|
||||
|
||||
// and revert back if necessary
|
||||
if (ourState != NULL && (settings->enableAutomaticModeSwitching) && (originalBulbMode != BulbMode::BULB_MODE_COLOR)) {
|
||||
switchMode(*ourState, originalBulbMode);
|
||||
}
|
||||
}
|
||||
|
||||
void FUT089PacketFormatter::updateColorWhite() {
|
||||
command(FUT089_ON, FUT089_WHITE_MODE);
|
||||
}
|
||||
|
||||
void FUT089PacketFormatter::enableNightMode() {
|
||||
uint8_t arg = groupCommandArg(OFF, groupId);
|
||||
command(FUT089_ON | 0x80, arg);
|
||||
}
|
||||
|
||||
BulbId FUT089PacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
|
||||
if (stateStore == NULL) {
|
||||
Serial.println(F("ERROR: stateStore not set. Prepare was not called! **THIS IS A BUG**"));
|
||||
BulbId fakeId(0, 0, REMOTE_TYPE_FUT089);
|
||||
return fakeId;
|
||||
}
|
||||
|
||||
uint8_t packetCopy[V2_PACKET_LEN];
|
||||
memcpy(packetCopy, packet, V2_PACKET_LEN);
|
||||
V2RFEncoding::decodeV2Packet(packetCopy);
|
||||
|
||||
BulbId bulbId(
|
||||
(packetCopy[2] << 8) | packetCopy[3],
|
||||
packetCopy[7],
|
||||
REMOTE_TYPE_FUT089
|
||||
);
|
||||
|
||||
uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
|
||||
uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];
|
||||
|
||||
if (command == FUT089_ON) {
|
||||
if ((packetCopy[V2_COMMAND_INDEX] & 0x80) == 0x80) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::NIGHT_MODE;
|
||||
} else if (arg == FUT089_MODE_SPEED_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_DOWN;
|
||||
} else if (arg == FUT089_MODE_SPEED_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_UP;
|
||||
} else if (arg == FUT089_WHITE_MODE) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::SET_WHITE;
|
||||
} else if (arg <= 8) { // Group is not reliably encoded in group byte. Extract from arg byte
|
||||
result[GroupStateFieldNames::STATE] = "ON";
|
||||
bulbId.groupId = arg;
|
||||
} else if (arg >= 9 && arg <= 17) {
|
||||
result[GroupStateFieldNames::STATE] = "OFF";
|
||||
bulbId.groupId = arg-9;
|
||||
}
|
||||
} else if (command == FUT089_COLOR) {
|
||||
uint8_t rescaledColor = (arg - FUT089_COLOR_OFFSET) % 0x100;
|
||||
uint16_t hue = Units::rescale<uint16_t, uint16_t>(rescaledColor, 360, 255.0);
|
||||
result[GroupStateFieldNames::HUE] = hue;
|
||||
} else if (command == FUT089_BRIGHTNESS) {
|
||||
uint8_t level = constrain(arg, 0, 100);
|
||||
result[GroupStateFieldNames::BRIGHTNESS] = Units::rescale<uint8_t, uint8_t>(level, 255, 100);
|
||||
// saturation == kelvin. arg ranges are the same, so can't distinguish
|
||||
// without using state
|
||||
} else if (command == FUT089_SATURATION) {
|
||||
const GroupState* state = stateStore->get(bulbId);
|
||||
|
||||
if (state != NULL && state->getBulbMode() == BULB_MODE_COLOR) {
|
||||
result[GroupStateFieldNames::SATURATION] = 100 - constrain(arg, 0, 100);
|
||||
} else {
|
||||
result[GroupStateFieldNames::COLOR_TEMP] = Units::whiteValToMireds(100 - arg, 100);
|
||||
}
|
||||
} else if (command == FUT089_MODE) {
|
||||
result[GroupStateFieldNames::MODE] = arg;
|
||||
} else {
|
||||
result["button_id"] = command;
|
||||
result["argument"] = arg;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
45
lib/MiLight/FUT089PacketFormatter.h
Normal file
45
lib/MiLight/FUT089PacketFormatter.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
#include <V2PacketFormatter.h>
|
||||
|
||||
#ifndef _FUT089_PACKET_FORMATTER_H
|
||||
#define _FUT089_PACKET_FORMATTER_H
|
||||
|
||||
#define FUT089_COLOR_OFFSET 0
|
||||
|
||||
enum MiLightFUT089Command {
|
||||
FUT089_ON = 0x01,
|
||||
FUT089_OFF = 0x01,
|
||||
FUT089_COLOR = 0x02,
|
||||
FUT089_BRIGHTNESS = 0x05,
|
||||
FUT089_MODE = 0x06,
|
||||
FUT089_KELVIN = 0x07, // Controls Kelvin when in White mode
|
||||
FUT089_SATURATION = 0x07 // Controls Saturation when in Color mode
|
||||
};
|
||||
|
||||
enum MiLightFUT089Arguments {
|
||||
FUT089_MODE_SPEED_UP = 0x12,
|
||||
FUT089_MODE_SPEED_DOWN = 0x13,
|
||||
FUT089_WHITE_MODE = 0x14
|
||||
};
|
||||
|
||||
class FUT089PacketFormatter : public V2PacketFormatter {
|
||||
public:
|
||||
FUT089PacketFormatter()
|
||||
: V2PacketFormatter(REMOTE_TYPE_FUT089, 0x25, 8) // protocol is 0x25, and there are 8 groups
|
||||
{ }
|
||||
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void updateHue(uint16_t value);
|
||||
virtual void updateColorRaw(uint8_t value);
|
||||
virtual void updateColorWhite();
|
||||
virtual void updateTemperature(uint8_t value);
|
||||
virtual void updateSaturation(uint8_t value);
|
||||
virtual void enableNightMode();
|
||||
|
||||
virtual void modeSpeedDown();
|
||||
virtual void modeSpeedUp();
|
||||
virtual void updateMode(uint8_t mode);
|
||||
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
};
|
||||
|
||||
#endif
|
58
lib/MiLight/FUT091PacketFormatter.cpp
Normal file
58
lib/MiLight/FUT091PacketFormatter.cpp
Normal file
|
@ -0,0 +1,58 @@
|
|||
#include <FUT091PacketFormatter.h>
|
||||
#include <V2RFEncoding.h>
|
||||
#include <Units.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
static const uint8_t BRIGHTNESS_SCALE_MAX = 0x97;
|
||||
static const uint8_t KELVIN_SCALE_MAX = 0xC5;
|
||||
|
||||
void FUT091PacketFormatter::updateBrightness(uint8_t value) {
|
||||
command(static_cast<uint8_t>(FUT091Command::BRIGHTNESS), V2PacketFormatter::tov2scale(value, BRIGHTNESS_SCALE_MAX, 2));
|
||||
}
|
||||
|
||||
void FUT091PacketFormatter::updateTemperature(uint8_t value) {
|
||||
command(static_cast<uint8_t>(FUT091Command::KELVIN), V2PacketFormatter::tov2scale(value, KELVIN_SCALE_MAX, 2, false));
|
||||
}
|
||||
|
||||
void FUT091PacketFormatter::enableNightMode() {
|
||||
uint8_t arg = groupCommandArg(OFF, groupId);
|
||||
command(static_cast<uint8_t>(FUT091Command::ON_OFF) | 0x80, arg);
|
||||
}
|
||||
|
||||
BulbId FUT091PacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
|
||||
uint8_t packetCopy[V2_PACKET_LEN];
|
||||
memcpy(packetCopy, packet, V2_PACKET_LEN);
|
||||
V2RFEncoding::decodeV2Packet(packetCopy);
|
||||
|
||||
BulbId bulbId(
|
||||
(packetCopy[2] << 8) | packetCopy[3],
|
||||
packetCopy[7],
|
||||
REMOTE_TYPE_FUT091
|
||||
);
|
||||
|
||||
uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
|
||||
uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];
|
||||
|
||||
if (command == (uint8_t)FUT091Command::ON_OFF) {
|
||||
if ((packetCopy[V2_COMMAND_INDEX] & 0x80) == 0x80) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::NIGHT_MODE;
|
||||
} else if (arg < 5) { // Group is not reliably encoded in group byte. Extract from arg byte
|
||||
result[GroupStateFieldNames::STATE] = "ON";
|
||||
bulbId.groupId = arg;
|
||||
} else {
|
||||
result[GroupStateFieldNames::STATE] = "OFF";
|
||||
bulbId.groupId = arg-5;
|
||||
}
|
||||
} else if (command == (uint8_t)FUT091Command::BRIGHTNESS) {
|
||||
uint8_t level = V2PacketFormatter::fromv2scale(arg, BRIGHTNESS_SCALE_MAX, 2, true);
|
||||
result[GroupStateFieldNames::BRIGHTNESS] = Units::rescale<uint8_t, uint8_t>(level, 255, 100);
|
||||
} else if (command == (uint8_t)FUT091Command::KELVIN) {
|
||||
uint8_t kelvin = V2PacketFormatter::fromv2scale(arg, KELVIN_SCALE_MAX, 2, false);
|
||||
result[GroupStateFieldNames::COLOR_TEMP] = Units::whiteValToMireds(kelvin, 100);
|
||||
} else {
|
||||
result["button_id"] = command;
|
||||
result["argument"] = arg;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
25
lib/MiLight/FUT091PacketFormatter.h
Normal file
25
lib/MiLight/FUT091PacketFormatter.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include <V2PacketFormatter.h>
|
||||
|
||||
#ifndef _FUT091_PACKET_FORMATTER_H
|
||||
#define _FUT091_PACKET_FORMATTER_H
|
||||
|
||||
enum class FUT091Command {
|
||||
ON_OFF = 0x01,
|
||||
BRIGHTNESS = 0x2,
|
||||
KELVIN = 0x03
|
||||
};
|
||||
|
||||
class FUT091PacketFormatter : public V2PacketFormatter {
|
||||
public:
|
||||
FUT091PacketFormatter()
|
||||
: V2PacketFormatter(REMOTE_TYPE_FUT091, 0x21, 4) // protocol is 0x21, and there are 4 groups
|
||||
{ }
|
||||
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void updateTemperature(uint8_t value);
|
||||
virtual void enableNightMode();
|
||||
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
};
|
||||
|
||||
#endif
|
669
lib/MiLight/MiLightClient.cpp
Normal file
669
lib/MiLight/MiLightClient.cpp
Normal file
|
@ -0,0 +1,669 @@
|
|||
#include <MiLightClient.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <Arduino.h>
|
||||
#include <RGBConverter.h>
|
||||
#include <Units.h>
|
||||
#include <TokenIterator.h>
|
||||
#include <ParsedColor.h>
|
||||
#include <MiLightCommands.h>
|
||||
#include <functional>
|
||||
|
||||
using namespace std::placeholders;
|
||||
|
||||
static const uint8_t STATUS_UNDEFINED = 255;
|
||||
|
||||
const char* MiLightClient::FIELD_ORDERINGS[] = {
|
||||
// These are handled manually
|
||||
// GroupStateFieldNames::STATE,
|
||||
// GroupStateFieldNames::STATUS,
|
||||
GroupStateFieldNames::HUE,
|
||||
GroupStateFieldNames::SATURATION,
|
||||
GroupStateFieldNames::KELVIN,
|
||||
GroupStateFieldNames::TEMPERATURE,
|
||||
GroupStateFieldNames::COLOR_TEMP,
|
||||
GroupStateFieldNames::MODE,
|
||||
GroupStateFieldNames::EFFECT,
|
||||
GroupStateFieldNames::COLOR,
|
||||
// Level/Brightness must be processed last because they're specific to a particular bulb mode.
|
||||
// So make sure bulb mode is set before applying level/brightness.
|
||||
GroupStateFieldNames::LEVEL,
|
||||
GroupStateFieldNames::BRIGHTNESS,
|
||||
GroupStateFieldNames::COMMAND,
|
||||
GroupStateFieldNames::COMMANDS
|
||||
};
|
||||
|
||||
const std::map<const char*, std::function<void(MiLightClient*, JsonVariant)>, MiLightClient::cmp_str> MiLightClient::FIELD_SETTERS = {
|
||||
{
|
||||
GroupStateFieldNames::STATUS,
|
||||
[](MiLightClient* client, JsonVariant val) {
|
||||
client->updateStatus(parseMilightStatus(val));
|
||||
}
|
||||
},
|
||||
{GroupStateFieldNames::LEVEL, &MiLightClient::updateBrightness},
|
||||
{
|
||||
GroupStateFieldNames::BRIGHTNESS,
|
||||
[](MiLightClient* client, uint16_t arg) {
|
||||
client->updateBrightness(Units::rescale<uint16_t, uint16_t>(arg, 100, 255));
|
||||
}
|
||||
},
|
||||
{GroupStateFieldNames::HUE, &MiLightClient::updateHue},
|
||||
{GroupStateFieldNames::SATURATION, &MiLightClient::updateSaturation},
|
||||
{GroupStateFieldNames::KELVIN, &MiLightClient::updateTemperature},
|
||||
{GroupStateFieldNames::TEMPERATURE, &MiLightClient::updateTemperature},
|
||||
{
|
||||
GroupStateFieldNames::COLOR_TEMP,
|
||||
[](MiLightClient* client, uint16_t arg) {
|
||||
client->updateTemperature(Units::miredsToWhiteVal(arg, 100));
|
||||
}
|
||||
},
|
||||
{GroupStateFieldNames::MODE, &MiLightClient::updateMode},
|
||||
{GroupStateFieldNames::COLOR, &MiLightClient::updateColor},
|
||||
{GroupStateFieldNames::EFFECT, &MiLightClient::handleEffect},
|
||||
{GroupStateFieldNames::COMMAND, &MiLightClient::handleCommand},
|
||||
{GroupStateFieldNames::COMMANDS, &MiLightClient::handleCommands}
|
||||
};
|
||||
|
||||
MiLightClient::MiLightClient(
|
||||
RadioSwitchboard& radioSwitchboard,
|
||||
PacketSender& packetSender,
|
||||
GroupStateStore* stateStore,
|
||||
Settings& settings,
|
||||
TransitionController& transitions
|
||||
) : radioSwitchboard(radioSwitchboard)
|
||||
, updateBeginHandler(NULL)
|
||||
, updateEndHandler(NULL)
|
||||
, stateStore(stateStore)
|
||||
, settings(settings)
|
||||
, packetSender(packetSender)
|
||||
, transitions(transitions)
|
||||
, repeatsOverride(0)
|
||||
{ }
|
||||
|
||||
void MiLightClient::setHeld(bool held) {
|
||||
currentRemote->packetFormatter->setHeld(held);
|
||||
}
|
||||
|
||||
void MiLightClient::prepare(
|
||||
const MiLightRemoteConfig* config,
|
||||
const uint16_t deviceId,
|
||||
const uint8_t groupId
|
||||
) {
|
||||
this->currentRemote = config;
|
||||
|
||||
if (deviceId >= 0 && groupId >= 0) {
|
||||
currentRemote->packetFormatter->prepare(deviceId, groupId);
|
||||
}
|
||||
|
||||
this->currentState = stateStore->get(deviceId, groupId, config->type);
|
||||
}
|
||||
|
||||
void MiLightClient::prepare(
|
||||
const MiLightRemoteType type,
|
||||
const uint16_t deviceId,
|
||||
const uint8_t groupId
|
||||
) {
|
||||
prepare(MiLightRemoteConfig::fromType(type), deviceId, groupId);
|
||||
}
|
||||
|
||||
void MiLightClient::updateColorRaw(const uint8_t color) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateColorRaw: Change color to %d\n"), color);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateColorRaw(color);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateHue(const uint16_t hue) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateHue: Change hue to %d\n"), hue);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateHue(hue);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateBrightness(const uint8_t brightness) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateBrightness: Change brightness to %d\n"), brightness);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateBrightness(brightness);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateMode(uint8_t mode) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateMode: Change mode to %d\n"), mode);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateMode(mode);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::nextMode() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::nextMode: Switch to next mode"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->nextMode();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::previousMode() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::previousMode: Switch to previous mode"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->previousMode();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::modeSpeedDown() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::modeSpeedDown: Speed down\n"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->modeSpeedDown();
|
||||
flushPacket();
|
||||
}
|
||||
void MiLightClient::modeSpeedUp() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::modeSpeedUp: Speed up"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->modeSpeedUp();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateStatus: Status %s, groupId %d\n"), status == MiLightStatus::OFF ? "OFF" : "ON", groupId);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateStatus(status, groupId);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateStatus(MiLightStatus status) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateStatus: Status %s\n"), status == MiLightStatus::OFF ? "OFF" : "ON");
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateStatus(status);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateSaturation(const uint8_t value) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateSaturation: Saturation %d\n"), value);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateSaturation(value);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateColorWhite() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::updateColorWhite: Color white"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateColorWhite();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::enableNightMode() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::enableNightMode: Night mode"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->enableNightMode();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::pair() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::pair: Pair"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->pair();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::unpair() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::unpair: Unpair"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->unpair();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::increaseBrightness() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::increaseBrightness: Increase brightness"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->increaseBrightness();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::decreaseBrightness() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::decreaseBrightness: Decrease brightness"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->decreaseBrightness();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::increaseTemperature() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::increaseTemperature: Increase temperature"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->increaseTemperature();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::decreaseTemperature() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.println(F("MiLightClient::decreaseTemperature: Decrease temperature"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->decreaseTemperature();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateTemperature(const uint8_t temperature) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::updateTemperature: Set temperature to %d\n"), temperature);
|
||||
#endif
|
||||
currentRemote->packetFormatter->updateTemperature(temperature);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::command(uint8_t command, uint8_t arg) {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::command: Execute command %d, argument %d\n"), command, arg);
|
||||
#endif
|
||||
currentRemote->packetFormatter->command(command, arg);
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::toggleStatus() {
|
||||
#ifdef DEBUG_CLIENT_COMMANDS
|
||||
Serial.printf_P(PSTR("MiLightClient::toggleStatus"));
|
||||
#endif
|
||||
currentRemote->packetFormatter->toggleStatus();
|
||||
flushPacket();
|
||||
}
|
||||
|
||||
void MiLightClient::updateColor(JsonVariant json) {
|
||||
ParsedColor color = ParsedColor::fromJson(json);
|
||||
|
||||
if (!color.success) {
|
||||
Serial.println(F("Error parsing color field, unrecognized format"));
|
||||
return;
|
||||
}
|
||||
|
||||
// We consider an RGB color "white" if all color intensities are roughly the
|
||||
// same value. An unscientific value of 10 (~4%) is chosen.
|
||||
if ( abs(color.r - color.g) < RGB_WHITE_THRESHOLD
|
||||
&& abs(color.g - color.b) < RGB_WHITE_THRESHOLD
|
||||
&& abs(color.r - color.b) < RGB_WHITE_THRESHOLD) {
|
||||
this->updateColorWhite();
|
||||
} else {
|
||||
this->updateHue(color.hue);
|
||||
this->updateSaturation(color.saturation);
|
||||
}
|
||||
}
|
||||
|
||||
void MiLightClient::update(JsonObject request) {
|
||||
if (this->updateBeginHandler) {
|
||||
this->updateBeginHandler();
|
||||
}
|
||||
|
||||
const JsonVariant status = this->extractStatus(request);
|
||||
const uint8_t parsedStatus = this->parseStatus(status);
|
||||
const JsonVariant jsonTransition = request[RequestKeys::TRANSITION];
|
||||
float transition = 0;
|
||||
|
||||
if (!jsonTransition.isNull()) {
|
||||
if (jsonTransition.is<float>()) {
|
||||
transition = jsonTransition.as<float>();
|
||||
} else if (jsonTransition.is<size_t>()) {
|
||||
transition = jsonTransition.as<size_t>();
|
||||
} else {
|
||||
Serial.println(F("MiLightClient - WARN: unsupported transition type. Must be float or int."));
|
||||
}
|
||||
}
|
||||
|
||||
JsonVariant brightness = request[GroupStateFieldNames::BRIGHTNESS];
|
||||
JsonVariant level = request[GroupStateFieldNames::LEVEL];
|
||||
const bool isBrightnessDefined = !brightness.isUndefined() || !level.isUndefined();
|
||||
|
||||
// Always turn on first
|
||||
if (parsedStatus == ON) {
|
||||
if (transition == 0) {
|
||||
this->updateStatus(ON);
|
||||
}
|
||||
// Don't do an "On" transition if the bulb is already on. The reasons for this are:
|
||||
// * Ambiguous what the behavior should be. Should it ramp to full brightness?
|
||||
// * HomeAssistant is only capable of sending transitions via the `light.turn_on`
|
||||
// service call, which ends up sending `{"status":"ON"}`. So transitions which
|
||||
// have nothing to do with the status will include an "ON" command.
|
||||
// If the user wants to transition brightness, they can just specify a brightness in
|
||||
// the same command. This avoids the need to make arbitrary calls on what the
|
||||
// behavior should be.
|
||||
else if (!currentState->isSetState() || !currentState->isOn()) {
|
||||
// If a brightness is defined, we'll want to transition to that. Status
|
||||
// transitions only ramp up/down to the max/min. Otherwise, just turn the bulb on
|
||||
// and let field transitions handle the rest.
|
||||
if (!isBrightnessDefined) {
|
||||
handleTransition(GroupStateField::STATUS, status, transition, 0);
|
||||
} else {
|
||||
this->updateStatus(ON);
|
||||
|
||||
if (! brightness.isUndefined()) {
|
||||
handleTransition(GroupStateField::BRIGHTNESS, brightness, transition, 0);
|
||||
} else if (! level.isUndefined()) {
|
||||
handleTransition(GroupStateField::LEVEL, level, transition, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const char* fieldName : FIELD_ORDERINGS) {
|
||||
if (request.containsKey(fieldName)) {
|
||||
auto handler = FIELD_SETTERS.find(fieldName);
|
||||
JsonVariant value = request[fieldName];
|
||||
|
||||
if (handler != FIELD_SETTERS.end()) {
|
||||
// No transition -- set field directly
|
||||
if (transition == 0) {
|
||||
handler->second(this, value);
|
||||
} else {
|
||||
GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
|
||||
|
||||
if ( !GroupStateFieldHelpers::isBrightnessField(field) // If field isn't brightness
|
||||
|| parsedStatus == STATUS_UNDEFINED // or if there was not a status field
|
||||
|| currentState->isOn() // or if bulb was already on
|
||||
) {
|
||||
handleTransition(field, value, transition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Raw packet command/args
|
||||
if (request.containsKey("button_id") && request.containsKey("argument")) {
|
||||
this->command(request["button_id"], request["argument"]);
|
||||
}
|
||||
|
||||
// Always turn off last
|
||||
if (parsedStatus == OFF) {
|
||||
if (transition == 0) {
|
||||
this->updateStatus(OFF);
|
||||
} else {
|
||||
handleTransition(GroupStateField::STATUS, status, transition);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->updateEndHandler) {
|
||||
this->updateEndHandler();
|
||||
}
|
||||
}
|
||||
|
||||
void MiLightClient::handleCommands(JsonArray commands) {
|
||||
if (! commands.isNull()) {
|
||||
for (size_t i = 0; i < commands.size(); i++) {
|
||||
this->handleCommand(commands[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MiLightClient::handleCommand(JsonVariant command) {
|
||||
String cmdName;
|
||||
JsonObject args;
|
||||
|
||||
if (command.is<JsonObject>()) {
|
||||
JsonObject cmdObj = command.as<JsonObject>();
|
||||
cmdName = cmdObj[GroupStateFieldNames::COMMAND].as<const char*>();
|
||||
args = cmdObj["args"];
|
||||
} else if (command.is<const char*>()) {
|
||||
cmdName = command.as<const char*>();
|
||||
}
|
||||
|
||||
if (cmdName == MiLightCommandNames::UNPAIR) {
|
||||
this->unpair();
|
||||
} else if (cmdName == MiLightCommandNames::PAIR) {
|
||||
this->pair();
|
||||
} else if (cmdName == MiLightCommandNames::SET_WHITE) {
|
||||
this->updateColorWhite();
|
||||
} else if (cmdName == MiLightCommandNames::NIGHT_MODE) {
|
||||
this->enableNightMode();
|
||||
} else if (cmdName == MiLightCommandNames::LEVEL_UP) {
|
||||
this->increaseBrightness();
|
||||
} else if (cmdName == MiLightCommandNames::LEVEL_DOWN) {
|
||||
this->decreaseBrightness();
|
||||
} else if (cmdName == "brightness_up") {
|
||||
this->increaseBrightness();
|
||||
} else if (cmdName == "brightness_down") {
|
||||
this->decreaseBrightness();
|
||||
} else if (cmdName == MiLightCommandNames::TEMPERATURE_UP) {
|
||||
this->increaseTemperature();
|
||||
} else if (cmdName == MiLightCommandNames::TEMPERATURE_DOWN) {
|
||||
this->decreaseTemperature();
|
||||
} else if (cmdName == MiLightCommandNames::NEXT_MODE) {
|
||||
this->nextMode();
|
||||
} else if (cmdName == MiLightCommandNames::PREVIOUS_MODE) {
|
||||
this->previousMode();
|
||||
} else if (cmdName == MiLightCommandNames::MODE_SPEED_DOWN) {
|
||||
this->modeSpeedDown();
|
||||
} else if (cmdName == MiLightCommandNames::MODE_SPEED_UP) {
|
||||
this->modeSpeedUp();
|
||||
} else if (cmdName == MiLightCommandNames::TOGGLE) {
|
||||
this->toggleStatus();
|
||||
} else if (cmdName == MiLightCommandNames::TRANSITION) {
|
||||
StaticJsonDocument<100> fakedoc;
|
||||
this->handleTransition(args, fakedoc);
|
||||
}
|
||||
}
|
||||
|
||||
void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, float duration, int16_t startValue) {
|
||||
BulbId bulbId = currentRemote->packetFormatter->currentBulbId();
|
||||
std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
|
||||
|
||||
if (currentState == nullptr) {
|
||||
Serial.println(F("Error planning transition: could not find current bulb state."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentState->isSetField(field)) {
|
||||
Serial.println(F("Error planning transition: current state for field could not be determined"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (field == GroupStateField::COLOR) {
|
||||
ParsedColor currentColor = currentState->getColor();
|
||||
ParsedColor endColor = ParsedColor::fromJson(value);
|
||||
|
||||
transitionBuilder = transitions.buildColorTransition(
|
||||
bulbId,
|
||||
currentColor,
|
||||
endColor
|
||||
);
|
||||
} else if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
|
||||
uint8_t startLevel;
|
||||
MiLightStatus status = parseMilightStatus(value);
|
||||
|
||||
if (startValue == FETCH_VALUE_FROM_STATE || currentState->isOn()) {
|
||||
startLevel = currentState->getBrightness();
|
||||
} else {
|
||||
startLevel = startValue;
|
||||
}
|
||||
|
||||
transitionBuilder = transitions.buildStatusTransition(bulbId, status, startLevel);
|
||||
} else {
|
||||
uint16_t currentValue;
|
||||
uint16_t endValue = value;
|
||||
|
||||
if (startValue == FETCH_VALUE_FROM_STATE || currentState->isOn()) {
|
||||
currentValue = currentState->getParsedFieldValue(field);
|
||||
} else {
|
||||
currentValue = startValue;
|
||||
}
|
||||
|
||||
transitionBuilder = transitions.buildFieldTransition(
|
||||
bulbId,
|
||||
field,
|
||||
currentValue,
|
||||
endValue
|
||||
);
|
||||
}
|
||||
|
||||
if (transitionBuilder == nullptr) {
|
||||
Serial.printf_P(PSTR("Unsupported transition field: %s\n"), GroupStateFieldHelpers::getFieldName(field));
|
||||
return;
|
||||
}
|
||||
|
||||
transitionBuilder->setDuration(duration);
|
||||
transitions.addTransition(transitionBuilder->build());
|
||||
}
|
||||
|
||||
bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) {
|
||||
if (! args.containsKey(FS(TransitionParams::FIELD))
|
||||
|| ! args.containsKey(FS(TransitionParams::END_VALUE))) {
|
||||
responseObj[F("error")] = F("Ignoring transition missing required arguments");
|
||||
return false;
|
||||
}
|
||||
|
||||
const BulbId& bulbId = currentRemote->packetFormatter->currentBulbId();
|
||||
const char* fieldName = args[FS(TransitionParams::FIELD)];
|
||||
JsonVariant startValue = args[FS(TransitionParams::START_VALUE)];
|
||||
JsonVariant endValue = args[FS(TransitionParams::END_VALUE)];
|
||||
GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
|
||||
std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
|
||||
|
||||
if (field == GroupStateField::UNKNOWN) {
|
||||
char errorMsg[30];
|
||||
sprintf_P(errorMsg, PSTR("Unknown transition field: %s\n"), fieldName);
|
||||
responseObj[F("error")] = errorMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
// These fields can be transitioned directly.
|
||||
switch (field) {
|
||||
case GroupStateField::HUE:
|
||||
case GroupStateField::SATURATION:
|
||||
case GroupStateField::BRIGHTNESS:
|
||||
case GroupStateField::LEVEL:
|
||||
case GroupStateField::KELVIN:
|
||||
case GroupStateField::COLOR_TEMP:
|
||||
|
||||
transitionBuilder = transitions.buildFieldTransition(
|
||||
bulbId,
|
||||
field,
|
||||
startValue.isUndefined()
|
||||
? currentState->getParsedFieldValue(field)
|
||||
: startValue.as<uint16_t>(),
|
||||
endValue
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Color can be decomposed into hue/saturation and these can be transitioned separately
|
||||
if (field == GroupStateField::COLOR) {
|
||||
ParsedColor _startValue = startValue.isUndefined()
|
||||
? currentState->getColor()
|
||||
: ParsedColor::fromJson(startValue);
|
||||
ParsedColor endColor = ParsedColor::fromJson(endValue);
|
||||
|
||||
if (! _startValue.success) {
|
||||
responseObj[F("error")] = F("Transition - error parsing start color");
|
||||
return false;
|
||||
}
|
||||
if (! endColor.success) {
|
||||
responseObj[F("error")] = F("Transition - error parsing end color");
|
||||
return false;
|
||||
}
|
||||
|
||||
transitionBuilder = transitions.buildColorTransition(
|
||||
bulbId,
|
||||
_startValue,
|
||||
endColor
|
||||
);
|
||||
}
|
||||
|
||||
// Status is handled a little differently
|
||||
if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
|
||||
MiLightStatus toStatus = parseMilightStatus(endValue);
|
||||
uint8_t startLevel;
|
||||
if (currentState->isSetBrightness()) {
|
||||
startLevel = currentState->getBrightness();
|
||||
} else if (toStatus == ON) {
|
||||
startLevel = 0;
|
||||
} else {
|
||||
startLevel = 100;
|
||||
}
|
||||
|
||||
transitionBuilder = transitions.buildStatusTransition(bulbId, toStatus, startLevel);
|
||||
}
|
||||
|
||||
if (transitionBuilder == nullptr) {
|
||||
char errorMsg[30];
|
||||
sprintf_P(errorMsg, PSTR("Recognized, but unsupported transition field: %s\n"), fieldName);
|
||||
responseObj[F("error")] = errorMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.containsKey(FS(TransitionParams::DURATION))) {
|
||||
transitionBuilder->setDuration(args[FS(TransitionParams::DURATION)]);
|
||||
}
|
||||
if (args.containsKey(FS(TransitionParams::PERIOD))) {
|
||||
transitionBuilder->setPeriod(args[FS(TransitionParams::PERIOD)]);
|
||||
}
|
||||
|
||||
transitions.addTransition(transitionBuilder->build());
|
||||
return true;
|
||||
}
|
||||
|
||||
void MiLightClient::handleEffect(const String& effect) {
|
||||
if (effect == MiLightCommandNames::NIGHT_MODE) {
|
||||
this->enableNightMode();
|
||||
} else if (effect == "white" || effect == "white_mode") {
|
||||
this->updateColorWhite();
|
||||
} else { // assume we're trying to set mode
|
||||
this->updateMode(effect.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
JsonVariant MiLightClient::extractStatus(JsonObject object) {
|
||||
JsonVariant status;
|
||||
|
||||
if (object.containsKey(FS(GroupStateFieldNames::STATUS))) {
|
||||
return object[FS(GroupStateFieldNames::STATUS)];
|
||||
} else {
|
||||
return object[FS(GroupStateFieldNames::STATE)];
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t MiLightClient::parseStatus(JsonVariant val) {
|
||||
if (val.isUndefined()) {
|
||||
return STATUS_UNDEFINED;
|
||||
}
|
||||
|
||||
return parseMilightStatus(val);
|
||||
}
|
||||
|
||||
void MiLightClient::setRepeatsOverride(size_t repeats) {
|
||||
this->repeatsOverride = repeats;
|
||||
}
|
||||
|
||||
void MiLightClient::clearRepeatsOverride() {
|
||||
this->repeatsOverride = PacketSender::DEFAULT_PACKET_SENDS_VALUE;
|
||||
}
|
||||
|
||||
void MiLightClient::flushPacket() {
|
||||
PacketStream& stream = currentRemote->packetFormatter->buildPackets();
|
||||
|
||||
while (stream.hasNext()) {
|
||||
packetSender.enqueue(stream.next(), currentRemote, repeatsOverride);
|
||||
}
|
||||
|
||||
currentRemote->packetFormatter->reset();
|
||||
}
|
||||
|
||||
void MiLightClient::onUpdateBegin(EventHandler handler) {
|
||||
this->updateBeginHandler = handler;
|
||||
}
|
||||
|
||||
void MiLightClient::onUpdateEnd(EventHandler handler) {
|
||||
this->updateEndHandler = handler;
|
||||
}
|
147
lib/MiLight/MiLightClient.h
Normal file
147
lib/MiLight/MiLightClient.h
Normal file
|
@ -0,0 +1,147 @@
|
|||
#include <functional>
|
||||
#include <Arduino.h>
|
||||
#include <MiLightRadio.h>
|
||||
#include <MiLightRadioFactory.h>
|
||||
#include <MiLightRemoteConfig.h>
|
||||
#include <Settings.h>
|
||||
#include <GroupStateStore.h>
|
||||
#include <PacketSender.h>
|
||||
#include <TransitionController.h>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#ifndef _MILIGHTCLIENT_H
|
||||
#define _MILIGHTCLIENT_H
|
||||
|
||||
//#define DEBUG_PRINTF
|
||||
//#define DEBUG_CLIENT_COMMANDS // enable to show each individual change command (like hue, brightness, etc)
|
||||
|
||||
#define FS(str) (reinterpret_cast<const __FlashStringHelper*>(str))
|
||||
|
||||
namespace RequestKeys {
|
||||
static const char TRANSITION[] = "transition";
|
||||
};
|
||||
|
||||
namespace TransitionParams {
|
||||
static const char FIELD[] PROGMEM = "field";
|
||||
static const char START_VALUE[] PROGMEM = "start_value";
|
||||
static const char END_VALUE[] PROGMEM = "end_value";
|
||||
static const char DURATION[] PROGMEM = "duration";
|
||||
static const char PERIOD[] PROGMEM = "period";
|
||||
}
|
||||
|
||||
// Used to determine RGB colros that are approximately white
|
||||
#define RGB_WHITE_THRESHOLD 10
|
||||
|
||||
class MiLightClient {
|
||||
public:
|
||||
// Used to indicate that the start value for a transition should be fetched from current state
|
||||
static const int16_t FETCH_VALUE_FROM_STATE = -1;
|
||||
|
||||
MiLightClient(
|
||||
RadioSwitchboard& radioSwitchboard,
|
||||
PacketSender& packetSender,
|
||||
GroupStateStore* stateStore,
|
||||
Settings& settings,
|
||||
TransitionController& transitions
|
||||
);
|
||||
|
||||
~MiLightClient() { }
|
||||
|
||||
typedef std::function<void(void)> EventHandler;
|
||||
|
||||
void prepare(const MiLightRemoteConfig* remoteConfig, const uint16_t deviceId = -1, const uint8_t groupId = -1);
|
||||
void prepare(const MiLightRemoteType type, const uint16_t deviceId = -1, const uint8_t groupId = -1);
|
||||
|
||||
void setResendCount(const unsigned int resendCount);
|
||||
bool available();
|
||||
size_t read(uint8_t packet[]);
|
||||
void write(uint8_t packet[]);
|
||||
|
||||
void setHeld(bool held);
|
||||
|
||||
// Common methods
|
||||
void updateStatus(MiLightStatus status);
|
||||
void updateStatus(MiLightStatus status, uint8_t groupId);
|
||||
void pair();
|
||||
void unpair();
|
||||
void command(uint8_t command, uint8_t arg);
|
||||
void updateMode(uint8_t mode);
|
||||
void nextMode();
|
||||
void previousMode();
|
||||
void modeSpeedDown();
|
||||
void modeSpeedUp();
|
||||
void toggleStatus();
|
||||
|
||||
// RGBW methods
|
||||
void updateHue(const uint16_t hue);
|
||||
void updateBrightness(const uint8_t brightness);
|
||||
void updateColorWhite();
|
||||
void updateColorRaw(const uint8_t color);
|
||||
void enableNightMode();
|
||||
void updateColor(JsonVariant json);
|
||||
|
||||
// CCT methods
|
||||
void updateTemperature(const uint8_t colorTemperature);
|
||||
void decreaseTemperature();
|
||||
void increaseTemperature();
|
||||
void increaseBrightness();
|
||||
void decreaseBrightness();
|
||||
|
||||
void updateSaturation(const uint8_t saturation);
|
||||
|
||||
void update(JsonObject object);
|
||||
void handleCommand(JsonVariant command);
|
||||
void handleCommands(JsonArray commands);
|
||||
bool handleTransition(JsonObject args, JsonDocument& responseObj);
|
||||
void handleTransition(GroupStateField field, JsonVariant value, float duration, int16_t startValue = FETCH_VALUE_FROM_STATE);
|
||||
void handleEffect(const String& effect);
|
||||
|
||||
void onUpdateBegin(EventHandler handler);
|
||||
void onUpdateEnd(EventHandler handler);
|
||||
|
||||
size_t getNumRadios() const;
|
||||
std::shared_ptr<MiLightRadio> switchRadio(size_t radioIx);
|
||||
std::shared_ptr<MiLightRadio> switchRadio(const MiLightRemoteConfig* remoteConfig);
|
||||
MiLightRemoteConfig& currentRemoteConfig() const;
|
||||
|
||||
// Call to override the number of packet repeats that are sent. Clear with clearRepeatsOverride
|
||||
void setRepeatsOverride(size_t repeatsOverride);
|
||||
|
||||
// Clear the repeats override so that the default is used
|
||||
void clearRepeatsOverride();
|
||||
|
||||
uint8_t parseStatus(JsonVariant object);
|
||||
JsonVariant extractStatus(JsonObject object);
|
||||
|
||||
protected:
|
||||
struct cmp_str {
|
||||
bool operator()(char const *a, char const *b) const {
|
||||
return std::strcmp(a, b) < 0;
|
||||
}
|
||||
};
|
||||
static const std::map<const char*, std::function<void(MiLightClient*, JsonVariant)>, cmp_str> FIELD_SETTERS;
|
||||
static const char* FIELD_ORDERINGS[];
|
||||
|
||||
RadioSwitchboard& radioSwitchboard;
|
||||
std::vector<std::shared_ptr<MiLightRadio>> radios;
|
||||
std::shared_ptr<MiLightRadio> currentRadio;
|
||||
const MiLightRemoteConfig* currentRemote;
|
||||
|
||||
EventHandler updateBeginHandler;
|
||||
EventHandler updateEndHandler;
|
||||
|
||||
GroupStateStore* stateStore;
|
||||
const GroupState* currentState;
|
||||
Settings& settings;
|
||||
PacketSender& packetSender;
|
||||
TransitionController& transitions;
|
||||
|
||||
// If set, override the number of packet repeats used.
|
||||
size_t repeatsOverride;
|
||||
|
||||
void flushPacket();
|
||||
};
|
||||
|
||||
#endif
|
108
lib/MiLight/MiLightRemoteConfig.cpp
Normal file
108
lib/MiLight/MiLightRemoteConfig.cpp
Normal file
|
@ -0,0 +1,108 @@
|
|||
#include <MiLightRemoteConfig.h>
|
||||
#include <MiLightRemoteType.h>
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: These should be in the same order as MiLightRemoteType.
|
||||
*/
|
||||
const MiLightRemoteConfig* MiLightRemoteConfig::ALL_REMOTES[] = {
|
||||
&FUT096Config, // rgbw
|
||||
&FUT007Config, // cct
|
||||
&FUT092Config, // rgb+cct
|
||||
&FUT098Config, // rgb
|
||||
&FUT089Config, // 8-group rgb+cct (b8, fut089)
|
||||
&FUT091Config,
|
||||
&FUT020Config
|
||||
};
|
||||
|
||||
const size_t MiLightRemoteConfig::NUM_REMOTES = size(ALL_REMOTES);
|
||||
|
||||
const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
|
||||
return fromType(MiLightRemoteTypeHelpers::remoteTypeFromString(type));
|
||||
}
|
||||
|
||||
const MiLightRemoteConfig* MiLightRemoteConfig::fromType(MiLightRemoteType type) {
|
||||
if (type == REMOTE_TYPE_UNKNOWN || type >= size(ALL_REMOTES)) {
|
||||
Serial.print(F("MiLightRemoteConfig::fromType: ERROR - tried to fetch remote config for unknown type: "));
|
||||
Serial.println(type);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return ALL_REMOTES[type];
|
||||
}
|
||||
|
||||
const MiLightRemoteConfig* MiLightRemoteConfig::fromReceivedPacket(
|
||||
const MiLightRadioConfig& radioConfig,
|
||||
const uint8_t* packet,
|
||||
const size_t len
|
||||
) {
|
||||
for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
|
||||
const MiLightRemoteConfig* config = MiLightRemoteConfig::ALL_REMOTES[i];
|
||||
if (&config->radioConfig == &radioConfig
|
||||
&& config->packetFormatter->canHandle(packet, len)) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// This can happen under normal circumstances, so not an error condition
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println(F("MiLightRemoteConfig::fromReceivedPacket: ERROR - tried to fetch remote config for unknown packet"));
|
||||
#endif
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const MiLightRemoteConfig FUT096Config( //rgbw
|
||||
new RgbwPacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[0],
|
||||
REMOTE_TYPE_RGBW,
|
||||
"rgbw",
|
||||
4
|
||||
);
|
||||
|
||||
const MiLightRemoteConfig FUT007Config( //cct
|
||||
new CctPacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[1],
|
||||
REMOTE_TYPE_CCT,
|
||||
"cct",
|
||||
4
|
||||
);
|
||||
|
||||
const MiLightRemoteConfig FUT091Config( //v2 cct
|
||||
new FUT091PacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[2],
|
||||
REMOTE_TYPE_FUT091,
|
||||
"fut091",
|
||||
4
|
||||
);
|
||||
|
||||
const MiLightRemoteConfig FUT092Config( //rgb+cct
|
||||
new RgbCctPacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[2],
|
||||
REMOTE_TYPE_RGB_CCT,
|
||||
"rgb_cct",
|
||||
4
|
||||
);
|
||||
|
||||
const MiLightRemoteConfig FUT089Config( //rgb+cct B8 / FUT089
|
||||
new FUT089PacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[2],
|
||||
REMOTE_TYPE_FUT089,
|
||||
"fut089",
|
||||
8
|
||||
);
|
||||
|
||||
const MiLightRemoteConfig FUT098Config( //rgb
|
||||
new RgbPacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[3],
|
||||
REMOTE_TYPE_RGB,
|
||||
"rgb",
|
||||
0
|
||||
);
|
||||
|
||||
const MiLightRemoteConfig FUT020Config(
|
||||
new FUT020PacketFormatter(),
|
||||
MiLightRadioConfig::ALL_CONFIGS[4],
|
||||
REMOTE_TYPE_FUT020,
|
||||
"fut020",
|
||||
0
|
||||
);
|
53
lib/MiLight/MiLightRemoteConfig.h
Normal file
53
lib/MiLight/MiLightRemoteConfig.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
#include <MiLightRadioConfig.h>
|
||||
#include <PacketFormatter.h>
|
||||
|
||||
#include <RgbwPacketFormatter.h>
|
||||
#include <RgbPacketFormatter.h>
|
||||
#include <RgbCctPacketFormatter.h>
|
||||
#include <CctPacketFormatter.h>
|
||||
#include <FUT089PacketFormatter.h>
|
||||
#include <FUT091PacketFormatter.h>
|
||||
#include <FUT020PacketFormatter.h>
|
||||
#include <PacketFormatter.h>
|
||||
|
||||
#ifndef _MILIGHT_REMOTE_CONFIG_H
|
||||
#define _MILIGHT_REMOTE_CONFIG_H
|
||||
|
||||
class MiLightRemoteConfig {
|
||||
public:
|
||||
MiLightRemoteConfig(
|
||||
PacketFormatter* packetFormatter,
|
||||
MiLightRadioConfig& radioConfig,
|
||||
const MiLightRemoteType type,
|
||||
const String name,
|
||||
const size_t numGroups
|
||||
) : packetFormatter(packetFormatter),
|
||||
radioConfig(radioConfig),
|
||||
type(type),
|
||||
name(name),
|
||||
numGroups(numGroups)
|
||||
{ }
|
||||
|
||||
PacketFormatter* const packetFormatter;
|
||||
const MiLightRadioConfig& radioConfig;
|
||||
const MiLightRemoteType type;
|
||||
const String name;
|
||||
const size_t numGroups;
|
||||
|
||||
static const MiLightRemoteConfig* fromType(MiLightRemoteType type);
|
||||
static const MiLightRemoteConfig* fromType(const String& type);
|
||||
static const MiLightRemoteConfig* fromReceivedPacket(const MiLightRadioConfig& radioConfig, const uint8_t* packet, const size_t len);
|
||||
|
||||
static const size_t NUM_REMOTES;
|
||||
static const MiLightRemoteConfig* ALL_REMOTES[];
|
||||
};
|
||||
|
||||
extern const MiLightRemoteConfig FUT096Config; //rgbw
|
||||
extern const MiLightRemoteConfig FUT007Config; //cct
|
||||
extern const MiLightRemoteConfig FUT092Config; //rgb+cct
|
||||
extern const MiLightRemoteConfig FUT089Config; //rgb+cct B8 / FUT089
|
||||
extern const MiLightRemoteConfig FUT098Config; //rgb
|
||||
extern const MiLightRemoteConfig FUT091Config; //v2 cct
|
||||
extern const MiLightRemoteConfig FUT020Config;
|
||||
|
||||
#endif
|
188
lib/MiLight/PacketFormatter.cpp
Normal file
188
lib/MiLight/PacketFormatter.cpp
Normal file
|
@ -0,0 +1,188 @@
|
|||
#include <PacketFormatter.h>
|
||||
|
||||
static uint8_t* PACKET_BUFFER = new uint8_t[PACKET_FORMATTER_BUFFER_SIZE];
|
||||
|
||||
PacketStream::PacketStream()
|
||||
: packetStream(PACKET_BUFFER),
|
||||
numPackets(0),
|
||||
packetLength(0),
|
||||
currentPacket(0)
|
||||
{ }
|
||||
|
||||
bool PacketStream::hasNext() {
|
||||
return currentPacket < numPackets;
|
||||
}
|
||||
|
||||
uint8_t* PacketStream::next() {
|
||||
uint8_t* packet = packetStream + (currentPacket * packetLength);
|
||||
currentPacket++;
|
||||
return packet;
|
||||
}
|
||||
|
||||
PacketFormatter::PacketFormatter(const MiLightRemoteType deviceType, const size_t packetLength, const size_t maxPackets)
|
||||
: deviceType(deviceType),
|
||||
packetLength(packetLength),
|
||||
numPackets(0),
|
||||
currentPacket(NULL),
|
||||
held(false)
|
||||
{
|
||||
packetStream.packetLength = packetLength;
|
||||
}
|
||||
|
||||
void PacketFormatter::initialize(GroupStateStore* stateStore, const Settings* settings) {
|
||||
this->stateStore = stateStore;
|
||||
this->settings = settings;
|
||||
}
|
||||
|
||||
bool PacketFormatter::canHandle(const uint8_t *packet, const size_t len) {
|
||||
return len == packetLength;
|
||||
}
|
||||
|
||||
void PacketFormatter::finalizePacket(uint8_t* packet) { }
|
||||
|
||||
void PacketFormatter::updateStatus(MiLightStatus status) {
|
||||
updateStatus(status, groupId);
|
||||
}
|
||||
|
||||
void PacketFormatter::toggleStatus() {
|
||||
const GroupState* state = stateStore->get(deviceId, groupId, deviceType);
|
||||
|
||||
if (state && state->isSetState() && state->getState() == MiLightStatus::ON) {
|
||||
updateStatus(MiLightStatus::OFF);
|
||||
} else {
|
||||
updateStatus(MiLightStatus::ON);
|
||||
}
|
||||
}
|
||||
|
||||
void PacketFormatter::setHeld(bool held) {
|
||||
this->held = held;
|
||||
}
|
||||
|
||||
void PacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) { }
|
||||
void PacketFormatter::updateBrightness(uint8_t value) { }
|
||||
void PacketFormatter::updateMode(uint8_t value) { }
|
||||
void PacketFormatter::modeSpeedDown() { }
|
||||
void PacketFormatter::modeSpeedUp() { }
|
||||
void PacketFormatter::nextMode() { }
|
||||
void PacketFormatter::previousMode() { }
|
||||
void PacketFormatter::command(uint8_t command, uint8_t arg) { }
|
||||
|
||||
void PacketFormatter::updateHue(uint16_t value) { }
|
||||
void PacketFormatter::updateColorRaw(uint8_t value) { }
|
||||
void PacketFormatter::updateColorWhite() { }
|
||||
|
||||
void PacketFormatter::increaseTemperature() { }
|
||||
void PacketFormatter::decreaseTemperature() { }
|
||||
void PacketFormatter::increaseBrightness() { }
|
||||
void PacketFormatter::decreaseBrightness() { }
|
||||
void PacketFormatter::enableNightMode() { }
|
||||
|
||||
void PacketFormatter::updateTemperature(uint8_t value) { }
|
||||
void PacketFormatter::updateSaturation(uint8_t value) { }
|
||||
|
||||
BulbId PacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
|
||||
return DEFAULT_BULB_ID;
|
||||
}
|
||||
|
||||
void PacketFormatter::pair() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
updateStatus(ON);
|
||||
}
|
||||
}
|
||||
|
||||
void PacketFormatter::unpair() {
|
||||
pair();
|
||||
}
|
||||
|
||||
PacketStream& PacketFormatter::buildPackets() {
|
||||
if (numPackets > 0) {
|
||||
finalizePacket(currentPacket);
|
||||
}
|
||||
|
||||
packetStream.numPackets = numPackets;
|
||||
packetStream.currentPacket = 0;
|
||||
|
||||
return packetStream;
|
||||
}
|
||||
|
||||
void PacketFormatter::valueByStepFunction(StepFunction increase, StepFunction decrease, uint8_t numSteps, uint8_t targetValue, int8_t knownValue) {
|
||||
StepFunction fn;
|
||||
size_t numCommands = 0;
|
||||
|
||||
// If current value is not known, drive down to minimum value. Then we can assume that we
|
||||
// know the state (it'll be 0).
|
||||
if (knownValue == -1) {
|
||||
for (size_t i = 0; i < numSteps; i++) {
|
||||
(this->*decrease)();
|
||||
}
|
||||
|
||||
fn = increase;
|
||||
numCommands = targetValue;
|
||||
} else if (targetValue < knownValue) {
|
||||
fn = decrease;
|
||||
numCommands = (knownValue - targetValue);
|
||||
} else if (targetValue > knownValue) {
|
||||
fn = increase;
|
||||
numCommands = (targetValue - knownValue);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get to the desired value
|
||||
for (size_t i = 0; i < numCommands; i++) {
|
||||
(this->*fn)();
|
||||
}
|
||||
}
|
||||
|
||||
void PacketFormatter::prepare(uint16_t deviceId, uint8_t groupId) {
|
||||
this->deviceId = deviceId;
|
||||
this->groupId = groupId;
|
||||
reset();
|
||||
}
|
||||
|
||||
void PacketFormatter::reset() {
|
||||
this->numPackets = 0;
|
||||
this->currentPacket = PACKET_BUFFER;
|
||||
this->held = false;
|
||||
}
|
||||
|
||||
void PacketFormatter::pushPacket() {
|
||||
if (numPackets > 0) {
|
||||
finalizePacket(currentPacket);
|
||||
}
|
||||
|
||||
// Make sure there's enough buffer to add another packet.
|
||||
if ((currentPacket + packetLength) >= PACKET_BUFFER + PACKET_FORMATTER_BUFFER_SIZE) {
|
||||
Serial.println(F("ERROR: packet buffer full! Cannot buffer a new packet. THIS IS A BUG!"));
|
||||
return;
|
||||
}
|
||||
|
||||
currentPacket = PACKET_BUFFER + (numPackets * packetLength);
|
||||
numPackets++;
|
||||
initializePacket(currentPacket);
|
||||
}
|
||||
|
||||
void PacketFormatter::format(uint8_t const* packet, char* buffer) {
|
||||
for (size_t i = 0; i < packetLength; i++) {
|
||||
sprintf_P(buffer, "%02X ", packet[i]);
|
||||
buffer += 3;
|
||||
}
|
||||
sprintf_P(buffer, "\n\n");
|
||||
}
|
||||
|
||||
void PacketFormatter::formatV1Packet(uint8_t const* packet, char* buffer) {
|
||||
buffer += sprintf_P(buffer, PSTR("Request type : %02X\n"), packet[0]) ;
|
||||
buffer += sprintf_P(buffer, PSTR("Device ID : %02X%02X\n"), packet[1], packet[2]);
|
||||
buffer += sprintf_P(buffer, PSTR("b1 : %02X\n"), packet[3]);
|
||||
buffer += sprintf_P(buffer, PSTR("b2 : %02X\n"), packet[4]);
|
||||
buffer += sprintf_P(buffer, PSTR("b3 : %02X\n"), packet[5]);
|
||||
buffer += sprintf_P(buffer, PSTR("Sequence Num. : %02X"), packet[6]);
|
||||
}
|
||||
|
||||
size_t PacketFormatter::getPacketLength() const {
|
||||
return packetLength;
|
||||
}
|
||||
|
||||
BulbId PacketFormatter::currentBulbId() const {
|
||||
return BulbId(deviceId, groupId, deviceType);
|
||||
}
|
120
lib/MiLight/PacketFormatter.h
Normal file
120
lib/MiLight/PacketFormatter.h
Normal file
|
@ -0,0 +1,120 @@
|
|||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
#include <functional>
|
||||
#include <MiLightRemoteType.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <GroupState.h>
|
||||
#include <GroupStateStore.h>
|
||||
#include <Settings.h>
|
||||
|
||||
#ifndef _PACKET_FORMATTER_H
|
||||
#define _PACKET_FORMATTER_H
|
||||
|
||||
// Most packets sent is for CCT bulbs, which always includes 10 down commands
|
||||
// and can include up to 10 up commands. CCT packets are 7 bytes.
|
||||
// (10 * 7) + (10 * 7) = 140
|
||||
#define PACKET_FORMATTER_BUFFER_SIZE 140
|
||||
|
||||
struct PacketStream {
|
||||
PacketStream();
|
||||
|
||||
uint8_t* next();
|
||||
bool hasNext();
|
||||
|
||||
uint8_t* packetStream;
|
||||
size_t numPackets;
|
||||
size_t packetLength;
|
||||
size_t currentPacket;
|
||||
};
|
||||
|
||||
class PacketFormatter {
|
||||
public:
|
||||
PacketFormatter(const MiLightRemoteType deviceType, const size_t packetLength, const size_t maxPackets = 1);
|
||||
|
||||
// Ideally these would be constructor parameters. We could accomplish this by
|
||||
// wrapping PacketFormaters in a factory, as Settings and StateStore are not
|
||||
// available at construction time.
|
||||
//
|
||||
// For now, just rely on the user calling this method.
|
||||
void initialize(GroupStateStore* stateStore, const Settings* settings);
|
||||
|
||||
typedef void (PacketFormatter::*StepFunction)();
|
||||
|
||||
virtual bool canHandle(const uint8_t* packet, const size_t len);
|
||||
|
||||
void updateStatus(MiLightStatus status);
|
||||
void toggleStatus();
|
||||
virtual void updateStatus(MiLightStatus status, uint8_t groupId);
|
||||
virtual void command(uint8_t command, uint8_t arg);
|
||||
|
||||
virtual void setHeld(bool held);
|
||||
|
||||
// Mode
|
||||
virtual void updateMode(uint8_t value);
|
||||
virtual void modeSpeedDown();
|
||||
virtual void modeSpeedUp();
|
||||
virtual void nextMode();
|
||||
virtual void previousMode();
|
||||
|
||||
virtual void pair();
|
||||
virtual void unpair();
|
||||
|
||||
// Color
|
||||
virtual void updateHue(uint16_t value);
|
||||
virtual void updateColorRaw(uint8_t value);
|
||||
virtual void updateColorWhite();
|
||||
|
||||
// White temperature
|
||||
virtual void increaseTemperature();
|
||||
virtual void decreaseTemperature();
|
||||
virtual void updateTemperature(uint8_t value);
|
||||
|
||||
// Brightness
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void increaseBrightness();
|
||||
virtual void decreaseBrightness();
|
||||
virtual void enableNightMode();
|
||||
|
||||
virtual void updateSaturation(uint8_t value);
|
||||
|
||||
virtual void reset();
|
||||
|
||||
virtual PacketStream& buildPackets();
|
||||
virtual void prepare(uint16_t deviceId, uint8_t groupId);
|
||||
virtual void format(uint8_t const* packet, char* buffer);
|
||||
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
virtual BulbId currentBulbId() const;
|
||||
|
||||
static void formatV1Packet(uint8_t const* packet, char* buffer);
|
||||
|
||||
size_t getPacketLength() const;
|
||||
|
||||
protected:
|
||||
const MiLightRemoteType deviceType;
|
||||
size_t packetLength;
|
||||
size_t numPackets;
|
||||
uint8_t* currentPacket;
|
||||
bool held;
|
||||
uint16_t deviceId;
|
||||
uint8_t groupId;
|
||||
uint8_t sequenceNum;
|
||||
PacketStream packetStream;
|
||||
GroupStateStore* stateStore = NULL;
|
||||
const Settings* settings = NULL;
|
||||
|
||||
void pushPacket();
|
||||
|
||||
// Get field into a desired state using only increment/decrement commands. Do this by:
|
||||
// 1. Driving it down to its minimum value
|
||||
// 2. Applying the appropriate number of increase commands to get it to the desired
|
||||
// value.
|
||||
// If the current state is already known, take that into account and apply the exact
|
||||
// number of rpeeats for the appropriate command.
|
||||
void valueByStepFunction(StepFunction increase, StepFunction decrease, uint8_t numSteps, uint8_t targetValue, int8_t knownValue = -1);
|
||||
|
||||
virtual void initializePacket(uint8_t* packetStart) = 0;
|
||||
virtual void finalizePacket(uint8_t* packet);
|
||||
};
|
||||
|
||||
#endif
|
42
lib/MiLight/PacketQueue.cpp
Normal file
42
lib/MiLight/PacketQueue.cpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
#include <PacketQueue.h>
|
||||
|
||||
PacketQueue::PacketQueue()
|
||||
: droppedPackets(0)
|
||||
{ }
|
||||
|
||||
void PacketQueue::push(const uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride) {
|
||||
std::shared_ptr<QueuedPacket> qp = checkoutPacket();
|
||||
memcpy(qp->packet, packet, remoteConfig->packetFormatter->getPacketLength());
|
||||
qp->remoteConfig = remoteConfig;
|
||||
qp->repeatsOverride = repeatsOverride;
|
||||
}
|
||||
|
||||
bool PacketQueue::isEmpty() const {
|
||||
return queue.size() == 0;
|
||||
}
|
||||
|
||||
size_t PacketQueue::getDroppedPacketCount() const {
|
||||
return droppedPackets;
|
||||
}
|
||||
|
||||
std::shared_ptr<QueuedPacket> PacketQueue::pop() {
|
||||
return queue.shift();
|
||||
}
|
||||
|
||||
std::shared_ptr<QueuedPacket> PacketQueue::checkoutPacket() {
|
||||
if (queue.size() == MILIGHT_MAX_QUEUED_PACKETS) {
|
||||
++droppedPackets;
|
||||
return queue.getLast();
|
||||
} else {
|
||||
std::shared_ptr<QueuedPacket> packet = std::make_shared<QueuedPacket>();
|
||||
queue.add(packet);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
void PacketQueue::checkinPacket(std::shared_ptr<QueuedPacket> packet) {
|
||||
}
|
||||
|
||||
size_t PacketQueue::size() const {
|
||||
return queue.size();
|
||||
}
|
36
lib/MiLight/PacketQueue.h
Normal file
36
lib/MiLight/PacketQueue.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <CircularBuffer.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <MiLightRemoteConfig.h>
|
||||
|
||||
#ifndef MILIGHT_MAX_QUEUED_PACKETS
|
||||
#define MILIGHT_MAX_QUEUED_PACKETS 20
|
||||
#endif
|
||||
|
||||
struct QueuedPacket {
|
||||
uint8_t packet[MILIGHT_MAX_PACKET_LENGTH];
|
||||
const MiLightRemoteConfig* remoteConfig;
|
||||
size_t repeatsOverride;
|
||||
};
|
||||
|
||||
class PacketQueue {
|
||||
public:
|
||||
PacketQueue();
|
||||
|
||||
void push(const uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride);
|
||||
std::shared_ptr<QueuedPacket> pop();
|
||||
bool isEmpty() const;
|
||||
size_t size() const;
|
||||
size_t getDroppedPacketCount() const;
|
||||
|
||||
private:
|
||||
size_t droppedPackets;
|
||||
|
||||
std::shared_ptr<QueuedPacket> checkoutPacket();
|
||||
void checkinPacket(std::shared_ptr<QueuedPacket> packet);
|
||||
|
||||
LinkedList<std::shared_ptr<QueuedPacket>> queue;
|
||||
};
|
125
lib/MiLight/PacketSender.cpp
Normal file
125
lib/MiLight/PacketSender.cpp
Normal file
|
@ -0,0 +1,125 @@
|
|||
#include <PacketSender.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
|
||||
PacketSender::PacketSender(
|
||||
RadioSwitchboard& radioSwitchboard,
|
||||
Settings& settings,
|
||||
PacketSentHandler packetSentHandler
|
||||
) : radioSwitchboard(radioSwitchboard)
|
||||
, settings(settings)
|
||||
, currentPacket(nullptr)
|
||||
, packetRepeatsRemaining(0)
|
||||
, packetSentHandler(packetSentHandler)
|
||||
, lastSend(0)
|
||||
, currentResendCount(settings.packetRepeats)
|
||||
, throttleMultiplier(
|
||||
std::ceil(
|
||||
(settings.packetRepeatThrottleSensitivity / 1000.0) * settings.packetRepeats
|
||||
)
|
||||
)
|
||||
{ }
|
||||
|
||||
void PacketSender::enqueue(uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println("Enqueuing packet");
|
||||
#endif
|
||||
size_t repeats = repeatsOverride == DEFAULT_PACKET_SENDS_VALUE
|
||||
? this->currentResendCount
|
||||
: repeatsOverride;
|
||||
|
||||
queue.push(packet, remoteConfig, repeats);
|
||||
}
|
||||
|
||||
void PacketSender::loop() {
|
||||
// Switch to the next packet if we're done with the current one
|
||||
if (packetRepeatsRemaining == 0 && !queue.isEmpty()) {
|
||||
nextPacket();
|
||||
}
|
||||
|
||||
// If there's a packet we're handling, deal with it
|
||||
if (currentPacket != nullptr && packetRepeatsRemaining > 0) {
|
||||
handleCurrentPacket();
|
||||
}
|
||||
}
|
||||
|
||||
bool PacketSender::isSending() {
|
||||
return packetRepeatsRemaining > 0 || !queue.isEmpty();
|
||||
}
|
||||
|
||||
void PacketSender::nextPacket() {
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.printf("Switching to next packet, %d packets in queue\n", queue.size());
|
||||
#endif
|
||||
currentPacket = queue.pop();
|
||||
|
||||
if (currentPacket->repeatsOverride > 0) {
|
||||
packetRepeatsRemaining = currentPacket->repeatsOverride;
|
||||
} else {
|
||||
packetRepeatsRemaining = settings.packetRepeats;
|
||||
}
|
||||
|
||||
// Adjust resend count according to throttling rules
|
||||
updateResendCount();
|
||||
}
|
||||
|
||||
void PacketSender::handleCurrentPacket() {
|
||||
// Always switch radio. could've been listening in another context
|
||||
radioSwitchboard.switchRadio(currentPacket->remoteConfig);
|
||||
|
||||
size_t numToSend = std::min(packetRepeatsRemaining, settings.packetRepeatsPerLoop);
|
||||
sendRepeats(numToSend);
|
||||
packetRepeatsRemaining -= numToSend;
|
||||
|
||||
// If we're done sending this packet, fire the sent packet callback
|
||||
if (packetRepeatsRemaining == 0 && packetSentHandler != nullptr) {
|
||||
packetSentHandler(currentPacket->packet, *currentPacket->remoteConfig);
|
||||
}
|
||||
}
|
||||
|
||||
size_t PacketSender::queueLength() const {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
size_t PacketSender::droppedPackets() const {
|
||||
return queue.getDroppedPacketCount();
|
||||
}
|
||||
|
||||
void PacketSender::sendRepeats(size_t num) {
|
||||
size_t len = currentPacket->remoteConfig->packetFormatter->getPacketLength();
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.printf_P(PSTR("Sending packet (%d repeats): \n"), num);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
Serial.printf_P(PSTR("%02X "), currentPacket->packet[i]);
|
||||
}
|
||||
Serial.println();
|
||||
int iStart = millis();
|
||||
#endif
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
radioSwitchboard.write(currentPacket->packet, len);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
int iElapsed = millis() - iStart;
|
||||
Serial.print("Elapsed: ");
|
||||
Serial.println(iElapsed);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PacketSender::updateResendCount() {
|
||||
unsigned long now = millis();
|
||||
long millisSinceLastSend = now - lastSend;
|
||||
long x = (millisSinceLastSend - settings.packetRepeatThrottleThreshold);
|
||||
long delta = x * throttleMultiplier;
|
||||
int signedResends = static_cast<int>(this->currentResendCount) + delta;
|
||||
|
||||
if (signedResends < static_cast<int>(settings.packetRepeatMinimum)) {
|
||||
signedResends = settings.packetRepeatMinimum;
|
||||
} else if (signedResends > static_cast<int>(settings.packetRepeats)) {
|
||||
signedResends = settings.packetRepeats;
|
||||
}
|
||||
|
||||
this->currentResendCount = signedResends;
|
||||
this->lastSend = now;
|
||||
}
|
74
lib/MiLight/PacketSender.h
Normal file
74
lib/MiLight/PacketSender.h
Normal file
|
@ -0,0 +1,74 @@
|
|||
#pragma once
|
||||
|
||||
#include <MiLightRadioFactory.h>
|
||||
#include <MiLightRemoteConfig.h>
|
||||
#include <PacketQueue.h>
|
||||
#include <RadioSwitchboard.h>
|
||||
|
||||
class PacketSender {
|
||||
public:
|
||||
typedef std::function<void(uint8_t* packet, const MiLightRemoteConfig& config)> PacketSentHandler;
|
||||
static const size_t DEFAULT_PACKET_SENDS_VALUE = 0;
|
||||
|
||||
PacketSender(
|
||||
RadioSwitchboard& radioSwitchboard,
|
||||
Settings& settings,
|
||||
PacketSentHandler packetSentHandler
|
||||
);
|
||||
|
||||
void enqueue(uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride = 0);
|
||||
void loop();
|
||||
|
||||
// Return true if there are queued packets
|
||||
bool isSending();
|
||||
|
||||
// Return the number of queued packets
|
||||
size_t queueLength() const;
|
||||
size_t droppedPackets() const;
|
||||
|
||||
private:
|
||||
RadioSwitchboard& radioSwitchboard;
|
||||
Settings& settings;
|
||||
GroupStateStore* stateStore;
|
||||
PacketQueue queue;
|
||||
|
||||
// The current packet we're sending and the number of repeats left
|
||||
std::shared_ptr<QueuedPacket> currentPacket;
|
||||
size_t packetRepeatsRemaining;
|
||||
|
||||
// Handler called after packets are sent. Will not be called multiple times
|
||||
// per repeat.
|
||||
PacketSentHandler packetSentHandler;
|
||||
|
||||
// Send a batch of repeats for the current packet
|
||||
void handleCurrentPacket();
|
||||
|
||||
// Switch to the next packet in the queue
|
||||
void nextPacket();
|
||||
|
||||
// Send repeats of the current packet N times
|
||||
void sendRepeats(size_t num);
|
||||
|
||||
// Used to track auto repeat limiting
|
||||
unsigned long lastSend;
|
||||
uint8_t currentResendCount;
|
||||
|
||||
// This will be pre-computed, but is simply:
|
||||
//
|
||||
// (sensitivity / 1000.0) * R
|
||||
//
|
||||
// Where R is the base number of repeats.
|
||||
size_t throttleMultiplier;
|
||||
|
||||
/*
|
||||
* Calculates the number of resend packets based on when the last packet
|
||||
* was sent using this function:
|
||||
*
|
||||
* lastRepeatsValue + (millisSinceLastSend - THRESHOLD) * throttleMultiplier
|
||||
*
|
||||
* When the last send was more recent than THRESHOLD, the number of repeats
|
||||
* will be decreased to a minimum of zero. When less recent, it will be
|
||||
* increased up to a maximum of the default resend count.
|
||||
*/
|
||||
void updateResendCount();
|
||||
};
|
74
lib/MiLight/RadioSwitchboard.cpp
Normal file
74
lib/MiLight/RadioSwitchboard.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include <RadioSwitchboard.h>
|
||||
|
||||
RadioSwitchboard::RadioSwitchboard(
|
||||
std::shared_ptr<MiLightRadioFactory> radioFactory,
|
||||
GroupStateStore* stateStore,
|
||||
Settings& settings
|
||||
) {
|
||||
for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
|
||||
std::shared_ptr<MiLightRadio> radio = radioFactory->create(MiLightRadioConfig::ALL_CONFIGS[i]);
|
||||
radio->begin();
|
||||
radios.push_back(radio);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
|
||||
MiLightRemoteConfig::ALL_REMOTES[i]->packetFormatter->initialize(stateStore, &settings);
|
||||
}
|
||||
}
|
||||
|
||||
size_t RadioSwitchboard::getNumRadios() const {
|
||||
return radios.size();
|
||||
}
|
||||
|
||||
std::shared_ptr<MiLightRadio> RadioSwitchboard::switchRadio(size_t radioIx) {
|
||||
if (radioIx >= getNumRadios()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (this->currentRadio != radios[radioIx]) {
|
||||
this->currentRadio = radios[radioIx];
|
||||
this->currentRadio->configure();
|
||||
}
|
||||
|
||||
return this->currentRadio;
|
||||
}
|
||||
|
||||
std::shared_ptr<MiLightRadio> RadioSwitchboard::switchRadio(const MiLightRemoteConfig* remote) {
|
||||
std::shared_ptr<MiLightRadio> radio = NULL;
|
||||
|
||||
for (size_t i = 0; i < radios.size(); i++) {
|
||||
if (&this->radios[i]->config() == &remote->radioConfig) {
|
||||
radio = switchRadio(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return radio;
|
||||
}
|
||||
|
||||
void RadioSwitchboard::write(uint8_t* packet, size_t len) {
|
||||
if (this->currentRadio == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->currentRadio->write(packet, len);
|
||||
}
|
||||
|
||||
size_t RadioSwitchboard::read(uint8_t* packet) {
|
||||
if (currentRadio == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t length;
|
||||
currentRadio->read(packet, length);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
bool RadioSwitchboard::available() {
|
||||
if (currentRadio == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentRadio->available();
|
||||
}
|
27
lib/MiLight/RadioSwitchboard.h
Normal file
27
lib/MiLight/RadioSwitchboard.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <MiLightRadio.h>
|
||||
#include <MiLightRemoteConfig.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <MiLightRadioFactory.h>
|
||||
|
||||
class RadioSwitchboard {
|
||||
public:
|
||||
RadioSwitchboard(
|
||||
std::shared_ptr<MiLightRadioFactory> radioFactory,
|
||||
GroupStateStore* stateStore,
|
||||
Settings& settings
|
||||
);
|
||||
|
||||
std::shared_ptr<MiLightRadio> switchRadio(const MiLightRemoteConfig* remote);
|
||||
std::shared_ptr<MiLightRadio> switchRadio(size_t index);
|
||||
size_t getNumRadios() const;
|
||||
|
||||
bool available();
|
||||
void write(uint8_t* packet, size_t length);
|
||||
size_t read(uint8_t* packet);
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<MiLightRadio>> radios;
|
||||
std::shared_ptr<MiLightRadio> currentRadio;
|
||||
};
|
159
lib/MiLight/RgbCctPacketFormatter.cpp
Normal file
159
lib/MiLight/RgbCctPacketFormatter.cpp
Normal file
|
@ -0,0 +1,159 @@
|
|||
#include <RgbCctPacketFormatter.h>
|
||||
#include <V2RFEncoding.h>
|
||||
#include <Units.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
void RgbCctPacketFormatter::modeSpeedDown() {
|
||||
command(RGB_CCT_ON, RGB_CCT_MODE_SPEED_DOWN);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::modeSpeedUp() {
|
||||
command(RGB_CCT_ON, RGB_CCT_MODE_SPEED_UP);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::updateMode(uint8_t mode) {
|
||||
lastMode = mode;
|
||||
command(RGB_CCT_MODE, mode);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::nextMode() {
|
||||
updateMode((lastMode+1)%RGB_CCT_NUM_MODES);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::previousMode() {
|
||||
updateMode((lastMode-1)%RGB_CCT_NUM_MODES);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::updateBrightness(uint8_t brightness) {
|
||||
command(RGB_CCT_BRIGHTNESS, RGB_CCT_BRIGHTNESS_OFFSET + brightness);
|
||||
}
|
||||
|
||||
// change the hue (which may also change to color mode).
|
||||
void RgbCctPacketFormatter::updateHue(uint16_t value) {
|
||||
uint8_t remapped = Units::rescale(value, 255, 360);
|
||||
updateColorRaw(remapped);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::updateColorRaw(uint8_t value) {
|
||||
command(RGB_CCT_COLOR, RGB_CCT_COLOR_OFFSET + value);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::updateTemperature(uint8_t value) {
|
||||
// Packet scale is [0x94, 0x92, .. 0, .., 0xCE, 0xCC]. Increments of 2.
|
||||
// From coolest to warmest.
|
||||
uint8_t cmdValue = V2PacketFormatter::tov2scale(value, RGB_CCT_KELVIN_REMOTE_END, 2);
|
||||
|
||||
// when updating temperature, the bulb switches to white. If we are not already
|
||||
// in white mode, that makes changing temperature annoying because the current hue/mode
|
||||
// is lost. So lookup our current bulb mode, and if needed, reset the hue/mode after
|
||||
// changing the temperature
|
||||
const GroupState* ourState = this->stateStore->get(this->deviceId, this->groupId, REMOTE_TYPE_RGB_CCT);
|
||||
|
||||
// now make the temperature change
|
||||
command(RGB_CCT_KELVIN, cmdValue);
|
||||
|
||||
// and return to our original mode
|
||||
if (ourState != NULL) {
|
||||
BulbMode originalBulbMode = ourState->getBulbMode();
|
||||
|
||||
if ((settings->enableAutomaticModeSwitching) && (originalBulbMode != BulbMode::BULB_MODE_WHITE)) {
|
||||
switchMode(*ourState, originalBulbMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update saturation. This only works when in Color mode, so if not in color we switch to color,
|
||||
// make the change, and switch back again.
|
||||
void RgbCctPacketFormatter::updateSaturation(uint8_t value) {
|
||||
// look up our current mode
|
||||
const GroupState* ourState = this->stateStore->get(this->deviceId, this->groupId, REMOTE_TYPE_RGB_CCT);
|
||||
BulbMode originalBulbMode = BulbMode::BULB_MODE_WHITE;
|
||||
|
||||
if (ourState != NULL) {
|
||||
originalBulbMode = ourState->getBulbMode();
|
||||
|
||||
// are we already in white? If not, change to white
|
||||
if ((settings->enableAutomaticModeSwitching) && (originalBulbMode != BulbMode::BULB_MODE_COLOR)) {
|
||||
updateHue(ourState->getHue());
|
||||
}
|
||||
}
|
||||
|
||||
// now make the saturation change
|
||||
uint8_t remapped = value + RGB_CCT_SATURATION_OFFSET;
|
||||
command(RGB_CCT_SATURATION, remapped);
|
||||
|
||||
if (ourState != NULL) {
|
||||
if ((settings->enableAutomaticModeSwitching) && (originalBulbMode != BulbMode::BULB_MODE_COLOR)) {
|
||||
switchMode(*ourState, originalBulbMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::updateColorWhite() {
|
||||
// there is no direct white command, so let's look up our prior temperature and set that, which
|
||||
// causes the bulb to go white
|
||||
const GroupState* ourState = this->stateStore->get(this->deviceId, this->groupId, REMOTE_TYPE_RGB_CCT);
|
||||
uint8_t value =
|
||||
ourState == NULL
|
||||
? 0
|
||||
: V2PacketFormatter::tov2scale(ourState->getKelvin(), RGB_CCT_KELVIN_REMOTE_END, 2);
|
||||
|
||||
// issue command to set kelvin to prior value, which will drive to white
|
||||
command(RGB_CCT_KELVIN, value);
|
||||
}
|
||||
|
||||
void RgbCctPacketFormatter::enableNightMode() {
|
||||
uint8_t arg = groupCommandArg(OFF, groupId);
|
||||
command(RGB_CCT_ON | 0x80, arg);
|
||||
}
|
||||
|
||||
BulbId RgbCctPacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
|
||||
uint8_t packetCopy[V2_PACKET_LEN];
|
||||
memcpy(packetCopy, packet, V2_PACKET_LEN);
|
||||
V2RFEncoding::decodeV2Packet(packetCopy);
|
||||
|
||||
BulbId bulbId(
|
||||
(packetCopy[2] << 8) | packetCopy[3],
|
||||
packetCopy[7],
|
||||
REMOTE_TYPE_RGB_CCT
|
||||
);
|
||||
|
||||
uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
|
||||
uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];
|
||||
|
||||
if (command == RGB_CCT_ON) {
|
||||
if ((packetCopy[V2_COMMAND_INDEX] & 0x80) == 0x80) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::NIGHT_MODE;
|
||||
} else if (arg == RGB_CCT_MODE_SPEED_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_DOWN;
|
||||
} else if (arg == RGB_CCT_MODE_SPEED_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_UP;
|
||||
} else if (arg < 5) { // Group is not reliably encoded in group byte. Extract from arg byte
|
||||
result[GroupStateFieldNames::STATE] = "ON";
|
||||
bulbId.groupId = arg;
|
||||
} else {
|
||||
result[GroupStateFieldNames::STATE] = "OFF";
|
||||
bulbId.groupId = arg-5;
|
||||
}
|
||||
} else if (command == RGB_CCT_COLOR) {
|
||||
uint8_t rescaledColor = (arg - RGB_CCT_COLOR_OFFSET) % 0x100;
|
||||
uint16_t hue = Units::rescale<uint16_t, uint16_t>(rescaledColor, 360, 255.0);
|
||||
result[GroupStateFieldNames::HUE] = hue;
|
||||
} else if (command == RGB_CCT_KELVIN) {
|
||||
uint8_t temperature = V2PacketFormatter::fromv2scale(arg, RGB_CCT_KELVIN_REMOTE_END, 2);
|
||||
result[GroupStateFieldNames::COLOR_TEMP] = Units::whiteValToMireds(temperature, 100);
|
||||
// brightness == saturation
|
||||
} else if (command == RGB_CCT_BRIGHTNESS && arg >= (RGB_CCT_BRIGHTNESS_OFFSET - 15)) {
|
||||
uint8_t level = constrain(arg - RGB_CCT_BRIGHTNESS_OFFSET, 0, 100);
|
||||
result[GroupStateFieldNames::BRIGHTNESS] = Units::rescale<uint8_t, uint8_t>(level, 255, 100);
|
||||
} else if (command == RGB_CCT_SATURATION) {
|
||||
result[GroupStateFieldNames::SATURATION] = constrain(arg - RGB_CCT_SATURATION_OFFSET, 0, 100);
|
||||
} else if (command == RGB_CCT_MODE) {
|
||||
result[GroupStateFieldNames::MODE] = arg;
|
||||
} else {
|
||||
result["button_id"] = command;
|
||||
result["argument"] = arg;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
60
lib/MiLight/RgbCctPacketFormatter.h
Normal file
60
lib/MiLight/RgbCctPacketFormatter.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include <V2PacketFormatter.h>
|
||||
|
||||
#ifndef _RGB_CCT_PACKET_FORMATTER_H
|
||||
#define _RGB_CCT_PACKET_FORMATTER_H
|
||||
|
||||
#define RGB_CCT_NUM_MODES 9
|
||||
|
||||
#define RGB_CCT_COLOR_OFFSET 0x5F
|
||||
#define RGB_CCT_BRIGHTNESS_OFFSET 0x8F
|
||||
#define RGB_CCT_SATURATION_OFFSET 0xD
|
||||
#define RGB_CCT_KELVIN_OFFSET 0x94
|
||||
|
||||
// Remotes have a larger range
|
||||
#define RGB_CCT_KELVIN_REMOTE_START 0x94
|
||||
#define RGB_CCT_KELVIN_REMOTE_END 0xCC
|
||||
|
||||
enum MiLightRgbCctCommand {
|
||||
RGB_CCT_ON = 0x01,
|
||||
RGB_CCT_OFF = 0x01,
|
||||
RGB_CCT_COLOR = 0x02,
|
||||
RGB_CCT_KELVIN = 0x03,
|
||||
RGB_CCT_BRIGHTNESS = 0x04,
|
||||
RGB_CCT_SATURATION = 0x04,
|
||||
RGB_CCT_MODE = 0x05
|
||||
};
|
||||
|
||||
enum MiLightRgbCctArguments {
|
||||
RGB_CCT_MODE_SPEED_UP = 0x0A,
|
||||
RGB_CCT_MODE_SPEED_DOWN = 0x0B
|
||||
};
|
||||
|
||||
class RgbCctPacketFormatter : public V2PacketFormatter {
|
||||
public:
|
||||
RgbCctPacketFormatter()
|
||||
: V2PacketFormatter(REMOTE_TYPE_RGB_CCT, 0x20, 4),
|
||||
lastMode(0)
|
||||
{ }
|
||||
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void updateHue(uint16_t value);
|
||||
virtual void updateColorRaw(uint8_t value);
|
||||
virtual void updateColorWhite();
|
||||
virtual void updateTemperature(uint8_t value);
|
||||
virtual void updateSaturation(uint8_t value);
|
||||
virtual void enableNightMode();
|
||||
|
||||
virtual void modeSpeedDown();
|
||||
virtual void modeSpeedUp();
|
||||
virtual void updateMode(uint8_t mode);
|
||||
virtual void nextMode();
|
||||
virtual void previousMode();
|
||||
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
|
||||
protected:
|
||||
|
||||
uint8_t lastMode;
|
||||
};
|
||||
|
||||
#endif
|
129
lib/MiLight/RgbPacketFormatter.cpp
Normal file
129
lib/MiLight/RgbPacketFormatter.cpp
Normal file
|
@ -0,0 +1,129 @@
|
|||
#include <RgbPacketFormatter.h>
|
||||
#include <Units.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
void RgbPacketFormatter::initializePacket(uint8_t *packet) {
|
||||
size_t packetPtr = 0;
|
||||
|
||||
packet[packetPtr++] = 0xA4;
|
||||
packet[packetPtr++] = deviceId >> 8;
|
||||
packet[packetPtr++] = deviceId & 0xFF;
|
||||
packet[packetPtr++] = 0;
|
||||
packet[packetPtr++] = 0;
|
||||
packet[packetPtr++] = sequenceNum++;
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::pair() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
command(RGB_SPEED_UP, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::unpair() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
command(RGB_SPEED_UP | 0x10, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
|
||||
command(status == ON ? RGB_ON : RGB_OFF, 0);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::command(uint8_t command, uint8_t arg) {
|
||||
pushPacket();
|
||||
if (held) {
|
||||
command |= 0x80;
|
||||
}
|
||||
currentPacket[RGB_COMMAND_INDEX] = command;
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::updateHue(uint16_t value) {
|
||||
const int16_t remappedColor = (value + 40) % 360;
|
||||
updateColorRaw(Units::rescale(remappedColor, 255, 360));
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::updateColorRaw(uint8_t value) {
|
||||
command(0, 0);
|
||||
currentPacket[RGB_COLOR_INDEX] = value;
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::updateBrightness(uint8_t value) {
|
||||
const GroupState* state = this->stateStore->get(deviceId, groupId, MiLightRemoteType::REMOTE_TYPE_RGB);
|
||||
int8_t knownValue = (state != NULL && state->isSetBrightness()) ? state->getBrightness() / RGB_INTERVALS : -1;
|
||||
|
||||
valueByStepFunction(
|
||||
&PacketFormatter::increaseBrightness,
|
||||
&PacketFormatter::decreaseBrightness,
|
||||
RGB_INTERVALS,
|
||||
value / RGB_INTERVALS,
|
||||
knownValue
|
||||
);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::increaseBrightness() {
|
||||
command(RGB_BRIGHTNESS_UP, 0);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::decreaseBrightness() {
|
||||
command(RGB_BRIGHTNESS_DOWN, 0);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::modeSpeedDown() {
|
||||
command(RGB_SPEED_DOWN, 0);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::modeSpeedUp() {
|
||||
command(RGB_SPEED_UP, 0);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::nextMode() {
|
||||
command(RGB_MODE_UP, 0);
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::previousMode() {
|
||||
command(RGB_MODE_DOWN, 0);
|
||||
}
|
||||
|
||||
BulbId RgbPacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
|
||||
uint8_t command = packet[RGB_COMMAND_INDEX] & 0x7F;
|
||||
|
||||
BulbId bulbId(
|
||||
(packet[1] << 8) | packet[2],
|
||||
0,
|
||||
REMOTE_TYPE_RGB
|
||||
);
|
||||
|
||||
if (command == RGB_ON) {
|
||||
result[GroupStateFieldNames::STATE] = "ON";
|
||||
} else if (command == RGB_OFF) {
|
||||
result[GroupStateFieldNames::STATE] = "OFF";
|
||||
} else if (command == 0) {
|
||||
uint16_t remappedColor = Units::rescale<uint16_t, uint16_t>(packet[RGB_COLOR_INDEX], 360.0, 255.0);
|
||||
remappedColor = (remappedColor + 320) % 360;
|
||||
result[GroupStateFieldNames::HUE] = remappedColor;
|
||||
} else if (command == RGB_MODE_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::PREVIOUS_MODE;
|
||||
} else if (command == RGB_MODE_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::NEXT_MODE;
|
||||
} else if (command == RGB_SPEED_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_DOWN;
|
||||
} else if (command == RGB_SPEED_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_UP;
|
||||
} else if (command == RGB_BRIGHTNESS_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = "brightness_down";
|
||||
} else if (command == RGB_BRIGHTNESS_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = "brightness_up";
|
||||
} else {
|
||||
result["button_id"] = command;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
||||
|
||||
void RgbPacketFormatter::format(uint8_t const* packet, char* buffer) {
|
||||
buffer += sprintf_P(buffer, PSTR("b0 : %02X\n"), packet[0]);
|
||||
buffer += sprintf_P(buffer, PSTR("ID : %02X%02X\n"), packet[1], packet[2]);
|
||||
buffer += sprintf_P(buffer, PSTR("Color : %02X\n"), packet[3]);
|
||||
buffer += sprintf_P(buffer, PSTR("Command : %02X\n"), packet[4]);
|
||||
buffer += sprintf_P(buffer, PSTR("Sequence : %02X\n"), packet[5]);
|
||||
}
|
47
lib/MiLight/RgbPacketFormatter.h
Normal file
47
lib/MiLight/RgbPacketFormatter.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
#include <PacketFormatter.h>
|
||||
|
||||
#ifndef _RGB_PACKET_FORMATTER_H
|
||||
#define _RGB_PACKET_FORMATTER_H
|
||||
|
||||
#define RGB_COMMAND_INDEX 4
|
||||
#define RGB_COLOR_INDEX 3
|
||||
#define RGB_INTERVALS 10
|
||||
|
||||
enum MiLightRgbButton {
|
||||
RGB_OFF = 0x01,
|
||||
RGB_ON = 0x02,
|
||||
RGB_BRIGHTNESS_UP = 0x03,
|
||||
RGB_BRIGHTNESS_DOWN = 0x04,
|
||||
RGB_SPEED_UP = 0x05,
|
||||
RGB_SPEED_DOWN = 0x06,
|
||||
RGB_MODE_UP = 0x07,
|
||||
RGB_MODE_DOWN = 0x08,
|
||||
RGB_PAIR = RGB_SPEED_UP
|
||||
};
|
||||
|
||||
class RgbPacketFormatter : public PacketFormatter {
|
||||
public:
|
||||
RgbPacketFormatter()
|
||||
: PacketFormatter(REMOTE_TYPE_RGB, 6, 20)
|
||||
{ }
|
||||
|
||||
virtual void updateStatus(MiLightStatus status, uint8_t groupId);
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void increaseBrightness();
|
||||
virtual void decreaseBrightness();
|
||||
virtual void command(uint8_t command, uint8_t arg);
|
||||
virtual void updateHue(uint16_t value);
|
||||
virtual void updateColorRaw(uint8_t value);
|
||||
virtual void format(uint8_t const* packet, char* buffer);
|
||||
virtual void pair();
|
||||
virtual void unpair();
|
||||
virtual void modeSpeedDown();
|
||||
virtual void modeSpeedUp();
|
||||
virtual void nextMode();
|
||||
virtual void previousMode();
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
|
||||
virtual void initializePacket(uint8_t* packet);
|
||||
};
|
||||
|
||||
#endif
|
160
lib/MiLight/RgbwPacketFormatter.cpp
Normal file
160
lib/MiLight/RgbwPacketFormatter.cpp
Normal file
|
@ -0,0 +1,160 @@
|
|||
#include <RgbwPacketFormatter.h>
|
||||
#include <Units.h>
|
||||
#include <MiLightCommands.h>
|
||||
|
||||
#define STATUS_COMMAND(status, groupId) ( RGBW_GROUP_1_ON + (((groupId) - 1)*2) + (status) )
|
||||
#define GROUP_FOR_STATUS_COMMAND(buttonId) ( ((buttonId) - 1) / 2 )
|
||||
#define STATUS_FOR_COMMAND(buttonId) ( ((buttonId) % 2) == 0 ? OFF : ON )
|
||||
|
||||
bool RgbwPacketFormatter::canHandle(const uint8_t *packet, const size_t len) {
|
||||
return len == packetLength && (packet[0] & 0xF0) == RGBW_PROTOCOL_ID_BYTE;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::initializePacket(uint8_t* packet) {
|
||||
size_t packetPtr = 0;
|
||||
|
||||
packet[packetPtr++] = RGBW_PROTOCOL_ID_BYTE;
|
||||
packet[packetPtr++] = deviceId >> 8;
|
||||
packet[packetPtr++] = deviceId & 0xFF;
|
||||
packet[packetPtr++] = 0;
|
||||
packet[packetPtr++] = (groupId & 0x07);
|
||||
packet[packetPtr++] = 0;
|
||||
packet[packetPtr++] = sequenceNum++;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::unpair() {
|
||||
PacketFormatter::updateStatus(ON);
|
||||
updateColorWhite();
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::modeSpeedDown() {
|
||||
command(RGBW_SPEED_DOWN, 0);
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::modeSpeedUp() {
|
||||
command(RGBW_SPEED_UP, 0);
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::nextMode() {
|
||||
updateMode((currentMode() + 1) % RGBW_NUM_MODES);
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::previousMode() {
|
||||
updateMode((currentMode() + RGBW_NUM_MODES - 1) % RGBW_NUM_MODES);
|
||||
}
|
||||
|
||||
uint8_t RgbwPacketFormatter::currentMode() {
|
||||
const GroupState* state = stateStore->get(deviceId, groupId, REMOTE_TYPE_RGBW);
|
||||
return state != NULL ? state->getMode() : 0;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::updateMode(uint8_t mode) {
|
||||
command(RGBW_DISCO_MODE, 0);
|
||||
currentPacket[0] = RGBW_PROTOCOL_ID_BYTE | mode;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
|
||||
command(STATUS_COMMAND(status, groupId), 0);
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::updateBrightness(uint8_t value) {
|
||||
// Expect an input value in [0, 100]. Map it down to [0, 25].
|
||||
const uint8_t adjustedBrightness = Units::rescale(value, 25, 100);
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
command(RGBW_BRIGHTNESS, 0);
|
||||
currentPacket[RGBW_BRIGHTNESS_GROUP_INDEX] |= (packetBrightnessValue << 3);
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::command(uint8_t command, uint8_t arg) {
|
||||
pushPacket();
|
||||
if (held) {
|
||||
command |= 0x80;
|
||||
}
|
||||
currentPacket[RGBW_COMMAND_INDEX] = command;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::updateHue(uint16_t value) {
|
||||
const int16_t remappedColor = (value + 40) % 360;
|
||||
updateColorRaw(Units::rescale(remappedColor, 255, 360));
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::updateColorRaw(uint8_t value) {
|
||||
command(RGBW_COLOR, 0);
|
||||
currentPacket[RGBW_COLOR_INDEX] = value;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::updateColorWhite() {
|
||||
uint8_t button = RGBW_GROUP_1_MAX_LEVEL + ((groupId - 1)*2);
|
||||
command(button, 0);
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::enableNightMode() {
|
||||
uint8_t button = STATUS_COMMAND(OFF, groupId);
|
||||
|
||||
// Bulbs must be OFF for night mode to work in RGBW.
|
||||
// Turn it off if it isn't already off.
|
||||
const GroupState* state = stateStore->get(deviceId, groupId, REMOTE_TYPE_RGBW);
|
||||
if (state == NULL || state->getState() == MiLightStatus::ON) {
|
||||
command(button, 0);
|
||||
}
|
||||
|
||||
// Night mode command has 0x10 bit set, but is otherwise
|
||||
// a repeat of the OFF command.
|
||||
command(button | 0x10, 0);
|
||||
}
|
||||
|
||||
BulbId RgbwPacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
|
||||
uint8_t command = packet[RGBW_COMMAND_INDEX] & 0x7F;
|
||||
|
||||
BulbId bulbId(
|
||||
(packet[1] << 8) | packet[2],
|
||||
packet[RGBW_BRIGHTNESS_GROUP_INDEX] & 0x7,
|
||||
REMOTE_TYPE_RGBW
|
||||
);
|
||||
|
||||
if (command >= RGBW_ALL_ON && command <= RGBW_GROUP_4_OFF) {
|
||||
result[GroupStateFieldNames::STATE] = (STATUS_FOR_COMMAND(command) == ON) ? "ON" : "OFF";
|
||||
|
||||
// Determine group ID from button ID for on/off. The remote's state is from
|
||||
// the last packet sent, not the current one, and that can be wrong for
|
||||
// on/off commands.
|
||||
bulbId.groupId = GROUP_FOR_STATUS_COMMAND(command);
|
||||
} else if (command & 0x10) {
|
||||
if ((command % 2) == 0) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::NIGHT_MODE;
|
||||
} else {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::SET_WHITE;
|
||||
}
|
||||
bulbId.groupId = GROUP_FOR_STATUS_COMMAND(command & 0xF);
|
||||
} else if (command == RGBW_BRIGHTNESS) {
|
||||
uint8_t brightness = 31;
|
||||
brightness -= packet[RGBW_BRIGHTNESS_GROUP_INDEX] >> 3;
|
||||
brightness += 17;
|
||||
brightness %= 32;
|
||||
result[GroupStateFieldNames::BRIGHTNESS] = Units::rescale<uint8_t, uint8_t>(brightness, 255, 25);
|
||||
} else if (command == RGBW_COLOR) {
|
||||
uint16_t remappedColor = Units::rescale<uint16_t, uint16_t>(packet[RGBW_COLOR_INDEX], 360.0, 255.0);
|
||||
remappedColor = (remappedColor + 320) % 360;
|
||||
result[GroupStateFieldNames::HUE] = remappedColor;
|
||||
} else if (command == RGBW_SPEED_DOWN) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_DOWN;
|
||||
} else if (command == RGBW_SPEED_UP) {
|
||||
result[GroupStateFieldNames::COMMAND] = MiLightCommandNames::MODE_SPEED_UP;
|
||||
} else if (command == RGBW_DISCO_MODE) {
|
||||
result[GroupStateFieldNames::MODE] = packet[0] & ~RGBW_PROTOCOL_ID_BYTE;
|
||||
} else {
|
||||
result["button_id"] = command;
|
||||
}
|
||||
|
||||
return bulbId;
|
||||
}
|
||||
|
||||
void RgbwPacketFormatter::format(uint8_t const* packet, char* buffer) {
|
||||
PacketFormatter::formatV1Packet(packet, buffer);
|
||||
}
|
82
lib/MiLight/RgbwPacketFormatter.h
Normal file
82
lib/MiLight/RgbwPacketFormatter.h
Normal file
|
@ -0,0 +1,82 @@
|
|||
#include <PacketFormatter.h>
|
||||
|
||||
#ifndef _RGBW_PACKET_FORMATTER_H
|
||||
#define _RGBW_PACKET_FORMATTER_H
|
||||
|
||||
#define RGBW_PROTOCOL_ID_BYTE 0xB0
|
||||
|
||||
enum MiLightRgbwButton {
|
||||
RGBW_ALL_ON = 0x01,
|
||||
RGBW_ALL_OFF = 0x02,
|
||||
RGBW_GROUP_1_ON = 0x03,
|
||||
RGBW_GROUP_1_OFF = 0x04,
|
||||
RGBW_GROUP_2_ON = 0x05,
|
||||
RGBW_GROUP_2_OFF = 0x06,
|
||||
RGBW_GROUP_3_ON = 0x07,
|
||||
RGBW_GROUP_3_OFF = 0x08,
|
||||
RGBW_GROUP_4_ON = 0x09,
|
||||
RGBW_GROUP_4_OFF = 0x0A,
|
||||
RGBW_SPEED_UP = 0x0B,
|
||||
RGBW_SPEED_DOWN = 0x0C,
|
||||
RGBW_DISCO_MODE = 0x0D,
|
||||
RGBW_BRIGHTNESS = 0x0E,
|
||||
RGBW_COLOR = 0x0F,
|
||||
RGBW_ALL_MAX_LEVEL = 0x11,
|
||||
RGBW_ALL_MIN_LEVEL = 0x12,
|
||||
|
||||
// These are the only mechanism (that I know of) to disable RGB and set the
|
||||
// color to white.
|
||||
RGBW_GROUP_1_MAX_LEVEL = 0x13,
|
||||
RGBW_GROUP_1_MIN_LEVEL = 0x14,
|
||||
RGBW_GROUP_2_MAX_LEVEL = 0x15,
|
||||
RGBW_GROUP_2_MIN_LEVEL = 0x16,
|
||||
RGBW_GROUP_3_MAX_LEVEL = 0x17,
|
||||
RGBW_GROUP_3_MIN_LEVEL = 0x18,
|
||||
RGBW_GROUP_4_MAX_LEVEL = 0x19,
|
||||
RGBW_GROUP_4_MIN_LEVEL = 0x1A,
|
||||
|
||||
// Button codes for night mode. A long press on the corresponding OFF button
|
||||
// Not actually needed/used.
|
||||
RGBW_ALL_NIGHT = 0x12,
|
||||
RGBW_GROUP_1_NIGHT = 0x14,
|
||||
RGBW_GROUP_2_NIGHT = 0x16,
|
||||
RGBW_GROUP_3_NIGHT = 0x18,
|
||||
RGBW_GROUP_4_NIGHT = 0x1A,
|
||||
};
|
||||
|
||||
#define RGBW_COMMAND_INDEX 5
|
||||
#define RGBW_BRIGHTNESS_GROUP_INDEX 4
|
||||
#define RGBW_COLOR_INDEX 3
|
||||
#define RGBW_NUM_MODES 9
|
||||
|
||||
class RgbwPacketFormatter : public PacketFormatter {
|
||||
public:
|
||||
RgbwPacketFormatter()
|
||||
: PacketFormatter(REMOTE_TYPE_RGBW, 7)
|
||||
{ }
|
||||
|
||||
virtual bool canHandle(const uint8_t* packet, const size_t len);
|
||||
virtual void updateStatus(MiLightStatus status, uint8_t groupId);
|
||||
virtual void updateBrightness(uint8_t value);
|
||||
virtual void command(uint8_t command, uint8_t arg);
|
||||
virtual void updateHue(uint16_t value);
|
||||
virtual void updateColorRaw(uint8_t value);
|
||||
virtual void updateColorWhite();
|
||||
virtual void format(uint8_t const* packet, char* buffer);
|
||||
virtual void unpair();
|
||||
virtual void modeSpeedDown();
|
||||
virtual void modeSpeedUp();
|
||||
virtual void nextMode();
|
||||
virtual void previousMode();
|
||||
virtual void updateMode(uint8_t mode);
|
||||
virtual void enableNightMode();
|
||||
virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
|
||||
|
||||
virtual void initializePacket(uint8_t* packet);
|
||||
|
||||
protected:
|
||||
static bool isStatusCommand(const uint8_t command);
|
||||
uint8_t currentMode();
|
||||
};
|
||||
|
||||
#endif
|
144
lib/MiLight/V2PacketFormatter.cpp
Normal file
144
lib/MiLight/V2PacketFormatter.cpp
Normal file
|
@ -0,0 +1,144 @@
|
|||
#include <V2PacketFormatter.h>
|
||||
#include <V2RFEncoding.h>
|
||||
|
||||
#define GROUP_COMMAND_ARG(status, groupId, numGroups) ( groupId + (status == OFF ? (numGroups + 1) : 0) )
|
||||
|
||||
V2PacketFormatter::V2PacketFormatter(const MiLightRemoteType deviceType, uint8_t protocolId, uint8_t numGroups)
|
||||
: PacketFormatter(deviceType, 9),
|
||||
protocolId(protocolId),
|
||||
numGroups(numGroups)
|
||||
{ }
|
||||
|
||||
bool V2PacketFormatter::canHandle(const uint8_t *packet, const size_t packetLen) {
|
||||
uint8_t packetCopy[V2_PACKET_LEN];
|
||||
memcpy(packetCopy, packet, V2_PACKET_LEN);
|
||||
V2RFEncoding::decodeV2Packet(packetCopy);
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.printf_P(PSTR("Testing whether formater for ID %d can handle packet: with protocol ID %d...\n"), protocolId, packetCopy[V2_PROTOCOL_ID_INDEX]);
|
||||
#endif
|
||||
|
||||
return packetCopy[V2_PROTOCOL_ID_INDEX] == protocolId;
|
||||
}
|
||||
|
||||
void V2PacketFormatter::initializePacket(uint8_t* packet) {
|
||||
size_t packetPtr = 0;
|
||||
|
||||
// Always encode with 0x00 key. No utility in varying it.
|
||||
packet[packetPtr++] = 0x00;
|
||||
|
||||
packet[packetPtr++] = protocolId;
|
||||
packet[packetPtr++] = deviceId >> 8;
|
||||
packet[packetPtr++] = deviceId & 0xFF;
|
||||
packet[packetPtr++] = 0;
|
||||
packet[packetPtr++] = 0;
|
||||
packet[packetPtr++] = sequenceNum++;
|
||||
packet[packetPtr++] = groupId;
|
||||
packet[packetPtr++] = 0;
|
||||
}
|
||||
|
||||
void V2PacketFormatter::command(uint8_t command, uint8_t arg) {
|
||||
pushPacket();
|
||||
if (held) {
|
||||
command |= 0x80;
|
||||
}
|
||||
currentPacket[V2_COMMAND_INDEX] = command;
|
||||
currentPacket[V2_ARGUMENT_INDEX] = arg;
|
||||
}
|
||||
|
||||
void V2PacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
|
||||
command(0x01, GROUP_COMMAND_ARG(status, groupId, numGroups));
|
||||
}
|
||||
|
||||
void V2PacketFormatter::unpair() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
updateStatus(ON, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void V2PacketFormatter::finalizePacket(uint8_t* packet) {
|
||||
V2RFEncoding::encodeV2Packet(packet);
|
||||
}
|
||||
|
||||
void V2PacketFormatter::format(uint8_t const* packet, char* buffer) {
|
||||
buffer += sprintf_P(buffer, PSTR("Raw packet: "));
|
||||
for (size_t i = 0; i < packetLength; i++) {
|
||||
buffer += sprintf_P(buffer, PSTR("%02X "), packet[i]);
|
||||
}
|
||||
|
||||
uint8_t decodedPacket[packetLength];
|
||||
memcpy(decodedPacket, packet, packetLength);
|
||||
|
||||
V2RFEncoding::decodeV2Packet(decodedPacket);
|
||||
|
||||
buffer += sprintf_P(buffer, PSTR("\n\nDecoded:\n"));
|
||||
buffer += sprintf_P(buffer, PSTR("Key : %02X\n"), decodedPacket[0]);
|
||||
buffer += sprintf_P(buffer, PSTR("b1 : %02X\n"), decodedPacket[1]);
|
||||
buffer += sprintf_P(buffer, PSTR("ID : %02X%02X\n"), decodedPacket[2], decodedPacket[3]);
|
||||
buffer += sprintf_P(buffer, PSTR("Command : %02X\n"), decodedPacket[4]);
|
||||
buffer += sprintf_P(buffer, PSTR("Argument : %02X\n"), decodedPacket[5]);
|
||||
buffer += sprintf_P(buffer, PSTR("Sequence : %02X\n"), decodedPacket[6]);
|
||||
buffer += sprintf_P(buffer, PSTR("Group : %02X\n"), decodedPacket[7]);
|
||||
buffer += sprintf_P(buffer, PSTR("Checksum : %02X"), decodedPacket[8]);
|
||||
}
|
||||
|
||||
uint8_t V2PacketFormatter::groupCommandArg(MiLightStatus status, uint8_t groupId) {
|
||||
return GROUP_COMMAND_ARG(status, groupId, numGroups);
|
||||
}
|
||||
|
||||
// helper method to return a bulb to the prior state
|
||||
void V2PacketFormatter::switchMode(const GroupState& currentState, BulbMode desiredMode) {
|
||||
// revert back to the prior mode
|
||||
switch (desiredMode) {
|
||||
case BulbMode::BULB_MODE_COLOR:
|
||||
updateHue(currentState.getHue());
|
||||
break;
|
||||
case BulbMode::BULB_MODE_NIGHT:
|
||||
enableNightMode();
|
||||
break;
|
||||
case BulbMode::BULB_MODE_SCENE:
|
||||
updateMode(currentState.getMode());
|
||||
break;
|
||||
case BulbMode::BULB_MODE_WHITE:
|
||||
updateColorWhite();
|
||||
break;
|
||||
default:
|
||||
Serial.printf_P(PSTR("V2PacketFormatter::switchMode: Request to switch to unknown mode %d\n"), desiredMode);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint8_t V2PacketFormatter::tov2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse) {
|
||||
if (reverse) {
|
||||
value = 100 - value;
|
||||
}
|
||||
|
||||
return (value * interval) + endValue;
|
||||
}
|
||||
|
||||
uint8_t V2PacketFormatter::fromv2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse, uint8_t buffer) {
|
||||
value -= endValue;
|
||||
|
||||
// Deal with underflow
|
||||
if (value >= (0xFF - buffer)) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
value /= interval;
|
||||
|
||||
if (reverse) {
|
||||
value = 100 - value;
|
||||
}
|
||||
|
||||
if (value > 100) {
|
||||
// overflow
|
||||
if (value <= (100 + buffer)) {
|
||||
value = 100;
|
||||
// underflow (value is unsigned)
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
55
lib/MiLight/V2PacketFormatter.h
Normal file
55
lib/MiLight/V2PacketFormatter.h
Normal file
|
@ -0,0 +1,55 @@
|
|||
#include <inttypes.h>
|
||||
#include <PacketFormatter.h>
|
||||
|
||||
#ifndef _V2_PACKET_FORMATTER
|
||||
#define _V2_PACKET_FORMATTER
|
||||
|
||||
#define V2_PACKET_LEN 9
|
||||
|
||||
#define V2_PROTOCOL_ID_INDEX 1
|
||||
#define V2_COMMAND_INDEX 4
|
||||
#define V2_ARGUMENT_INDEX 5
|
||||
|
||||
// Default number of values to allow before and after strictly defined range for V2 scales
|
||||
#define V2_DEFAULT_RANGE_BUFFER 0x13
|
||||
|
||||
class V2PacketFormatter : public PacketFormatter {
|
||||
public:
|
||||
V2PacketFormatter(const MiLightRemoteType deviceType, uint8_t protocolId, uint8_t numGroups);
|
||||
|
||||
virtual bool canHandle(const uint8_t* packet, const size_t packetLen);
|
||||
virtual void initializePacket(uint8_t* packet);
|
||||
|
||||
virtual void updateStatus(MiLightStatus status, uint8_t group);
|
||||
virtual void command(uint8_t command, uint8_t arg);
|
||||
virtual void format(uint8_t const* packet, char* buffer);
|
||||
virtual void unpair();
|
||||
|
||||
virtual void finalizePacket(uint8_t* packet);
|
||||
|
||||
uint8_t groupCommandArg(MiLightStatus status, uint8_t groupId);
|
||||
|
||||
/*
|
||||
* Some protocols have scales which have the following characteristics:
|
||||
* Start at some value X, goes down to 0, then up to Y.
|
||||
* eg:
|
||||
* 0x8F, 0x8D, ..., 0, 0x2, ..., 0x20
|
||||
* This is a parameterized method to convert from [0, 100] TO this scale
|
||||
*/
|
||||
static uint8_t tov2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse = true);
|
||||
|
||||
/*
|
||||
* Method to convert FROM the scale described above to [0, 100].
|
||||
*
|
||||
* An extra parameter is exposed: `buffer`, which allows for a range of values before/after the
|
||||
* max that will be mapped to 0 and 100, respectively.
|
||||
*/
|
||||
static uint8_t fromv2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse = true, uint8_t buffer = V2_DEFAULT_RANGE_BUFFER);
|
||||
|
||||
protected:
|
||||
const uint8_t protocolId;
|
||||
const uint8_t numGroups;
|
||||
void switchMode(const GroupState& currentState, BulbMode desiredMode);
|
||||
};
|
||||
|
||||
#endif
|
66
lib/MiLight/V2RFEncoding.cpp
Normal file
66
lib/MiLight/V2RFEncoding.cpp
Normal file
|
@ -0,0 +1,66 @@
|
|||
#include <V2RFEncoding.h>
|
||||
|
||||
#define V2_OFFSET(byte, key, jumpStart) ( \
|
||||
V2_OFFSETS[byte-1][key%4] \
|
||||
+ \
|
||||
((jumpStart > 0 && key >= jumpStart && key < jumpStart+0x80) ? 0x80 : 0) \
|
||||
)
|
||||
|
||||
uint8_t const V2RFEncoding::V2_OFFSETS[][4] = {
|
||||
{ 0x45, 0x1F, 0x14, 0x5C }, // request type
|
||||
{ 0x2B, 0xC9, 0xE3, 0x11 }, // id 1
|
||||
{ 0x6D, 0x5F, 0x8A, 0x2B }, // id 2
|
||||
{ 0xAF, 0x03, 0x1D, 0xF3 }, // command
|
||||
{ 0x1A, 0xE2, 0xF0, 0xD1 }, // argument
|
||||
{ 0x04, 0xD8, 0x71, 0x42 }, // sequence
|
||||
{ 0xAF, 0x04, 0xDD, 0x07 }, // group
|
||||
{ 0x61, 0x13, 0x38, 0x64 } // checksum
|
||||
};
|
||||
|
||||
uint8_t V2RFEncoding::xorKey(uint8_t key) {
|
||||
// Generate most significant nibble
|
||||
const uint8_t shift = (key & 0x0F) < 0x04 ? 0 : 1;
|
||||
const uint8_t x = (((key & 0xF0) >> 4) + shift + 6) % 8;
|
||||
const uint8_t msn = (((4 + x) ^ 1) & 0x0F) << 4;
|
||||
|
||||
// Generate least significant nibble
|
||||
const uint8_t lsn = ((((key & 0xF) + 4)^2) & 0x0F);
|
||||
|
||||
return ( msn | lsn );
|
||||
}
|
||||
|
||||
uint8_t V2RFEncoding::decodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2) {
|
||||
uint8_t value = byte - s2;
|
||||
value = value ^ xorKey;
|
||||
value = value - s1;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
uint8_t V2RFEncoding::encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2) {
|
||||
uint8_t value = byte + s1;
|
||||
value = value ^ xorKey;
|
||||
value = value + s2;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void V2RFEncoding::decodeV2Packet(uint8_t *packet) {
|
||||
uint8_t key = xorKey(packet[0]);
|
||||
|
||||
for (size_t i = 1; i <= 8; i++) {
|
||||
packet[i] = decodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0], V2_OFFSET_JUMP_START));
|
||||
}
|
||||
}
|
||||
|
||||
void V2RFEncoding::encodeV2Packet(uint8_t *packet) {
|
||||
uint8_t key = xorKey(packet[0]);
|
||||
uint8_t sum = key;
|
||||
|
||||
for (size_t i = 1; i <= 7; i++) {
|
||||
sum += packet[i];
|
||||
packet[i] = encodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0], V2_OFFSET_JUMP_START));
|
||||
}
|
||||
|
||||
packet[8] = encodeByte(sum, 2, key, V2_OFFSET(8, packet[0], 0));
|
||||
}
|
21
lib/MiLight/V2RFEncoding.h
Normal file
21
lib/MiLight/V2RFEncoding.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#ifndef _V2_RF_ENCODING_H
|
||||
#define _V2_RF_ENCODING_H
|
||||
|
||||
#define V2_OFFSET_JUMP_START 0x54
|
||||
|
||||
class V2RFEncoding {
|
||||
public:
|
||||
static void encodeV2Packet(uint8_t* packet);
|
||||
static void decodeV2Packet(uint8_t* packet);
|
||||
static uint8_t xorKey(uint8_t key);
|
||||
static uint8_t encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
|
||||
static uint8_t decodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
|
||||
|
||||
private:
|
||||
static uint8_t const V2_OFFSETS[][4];
|
||||
};
|
||||
|
||||
#endif
|
1024
lib/MiLightState/GroupState.cpp
Normal file
1024
lib/MiLightState/GroupState.cpp
Normal file
File diff suppressed because it is too large
Load diff
225
lib/MiLightState/GroupState.h
Normal file
225
lib/MiLightState/GroupState.h
Normal file
|
@ -0,0 +1,225 @@
|
|||
#include <stddef.h>
|
||||
#include <inttypes.h>
|
||||
#include <MiLightRemoteType.h>
|
||||
#include <MiLightStatus.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <GroupStateField.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <BulbId.h>
|
||||
#include <ParsedColor.h>
|
||||
|
||||
#ifndef _GROUP_STATE_H
|
||||
#define _GROUP_STATE_H
|
||||
|
||||
// enable to add debugging on state
|
||||
// #define DEBUG_STATE
|
||||
|
||||
enum BulbMode {
|
||||
BULB_MODE_WHITE,
|
||||
BULB_MODE_COLOR,
|
||||
BULB_MODE_SCENE,
|
||||
BULB_MODE_NIGHT
|
||||
};
|
||||
|
||||
enum class IncrementDirection : unsigned {
|
||||
INCREASE = 1,
|
||||
DECREASE = -1U
|
||||
};
|
||||
|
||||
class GroupState {
|
||||
public:
|
||||
static const GroupStateField ALL_PHYSICAL_FIELDS[];
|
||||
|
||||
GroupState();
|
||||
GroupState(const GroupState& other);
|
||||
GroupState& operator=(const GroupState& other);
|
||||
|
||||
// Convenience constructor that patches transient state from a previous GroupState,
|
||||
// and defaults with JSON state
|
||||
GroupState(const GroupState* previousState, JsonObject jsonState);
|
||||
|
||||
void initFields();
|
||||
|
||||
bool operator==(const GroupState& other) const;
|
||||
bool isEqualIgnoreDirty(const GroupState& other) const;
|
||||
void print(Stream& stream) const;
|
||||
|
||||
bool isSetField(GroupStateField field) const;
|
||||
uint16_t getFieldValue(GroupStateField field) const;
|
||||
uint16_t getParsedFieldValue(GroupStateField field) const;
|
||||
void setFieldValue(GroupStateField field, uint16_t value);
|
||||
bool clearField(GroupStateField field);
|
||||
|
||||
bool isSetScratchField(GroupStateField field) const;
|
||||
uint16_t getScratchFieldValue(GroupStateField field) const;
|
||||
void setScratchFieldValue(GroupStateField field, uint16_t value);
|
||||
|
||||
// 1 bit
|
||||
bool isSetState() const;
|
||||
MiLightStatus getState() const;
|
||||
bool setState(const MiLightStatus on);
|
||||
// Return true if status is ON or if the field is unset (i.e., defaults to ON)
|
||||
bool isOn() const;
|
||||
|
||||
// 7 bits
|
||||
bool isSetBrightness() const;
|
||||
uint8_t getBrightness() const;
|
||||
bool setBrightness(uint8_t brightness);
|
||||
bool clearBrightness();
|
||||
|
||||
// 8 bits
|
||||
bool isSetHue() const;
|
||||
uint16_t getHue() const;
|
||||
bool setHue(uint16_t hue);
|
||||
|
||||
// 7 bits
|
||||
bool isSetSaturation() const;
|
||||
uint8_t getSaturation() const;
|
||||
bool setSaturation(uint8_t saturation);
|
||||
|
||||
// 5 bits
|
||||
bool isSetMode() const;
|
||||
bool isSetEffect() const;
|
||||
uint8_t getMode() const;
|
||||
bool setMode(uint8_t mode);
|
||||
|
||||
// 7 bits
|
||||
bool isSetKelvin() const;
|
||||
uint8_t getKelvin() const;
|
||||
uint16_t getMireds() const;
|
||||
bool setKelvin(uint8_t kelvin);
|
||||
bool setMireds(uint16_t mireds);
|
||||
|
||||
// 3 bits
|
||||
bool isSetBulbMode() const;
|
||||
BulbMode getBulbMode() const;
|
||||
bool setBulbMode(BulbMode mode);
|
||||
|
||||
// 1 bit
|
||||
bool isSetNightMode() const;
|
||||
bool isNightMode() const;
|
||||
bool setNightMode(bool nightMode);
|
||||
|
||||
bool isDirty() const;
|
||||
inline bool setDirty();
|
||||
bool clearDirty();
|
||||
|
||||
bool isMqttDirty() const;
|
||||
inline bool setMqttDirty();
|
||||
bool clearMqttDirty();
|
||||
|
||||
// Clears all of the fields in THIS GroupState that have different values
|
||||
// than the provided group state.
|
||||
bool clearNonMatchingFields(const GroupState& other);
|
||||
|
||||
// Patches this state with ONLY the set fields in the other.
|
||||
void patch(const GroupState& other);
|
||||
|
||||
// Patches this state with the fields defined in the JSON state. Returns
|
||||
// true if there were any changes.
|
||||
bool patch(JsonObject state);
|
||||
|
||||
// It's a little weird to need to pass in a BulbId here. The purpose is to
|
||||
// support fields like DEVICE_ID, which aren't otherweise available to the
|
||||
// state in this class. The alternative is to have every GroupState object
|
||||
// keep a reference to its BulbId, which feels too heavy-weight.
|
||||
void applyField(JsonObject state, const BulbId& bulbId, GroupStateField field) const;
|
||||
void applyState(JsonObject state, const BulbId& bulbId, std::vector<GroupStateField>& fields) const;
|
||||
|
||||
// Attempt to keep track of increment commands in such a way that we can
|
||||
// know what state it's in. When we get an increment command (like "increase
|
||||
// brightness"):
|
||||
// 1. If there is no value in the scratch state: assume real state is in
|
||||
// the furthest value from the direction of the command. For example,
|
||||
// if we get "increase," assume the value was 0.
|
||||
// 2. If there is a value in the scratch state, apply the command to it.
|
||||
// For example, if we get "decrease," subtract 1 from the scratch.
|
||||
// 3. When scratch reaches a known extreme (either min or max), set the
|
||||
// persistent field to that value
|
||||
// 4. If there is already a known value for the state, apply it rather
|
||||
// than messing with scratch state.
|
||||
//
|
||||
// returns true if a (real, not scratch) state change was made
|
||||
bool applyIncrementCommand(GroupStateField field, IncrementDirection dir);
|
||||
|
||||
// Helpers that convert raw state values
|
||||
|
||||
// Return true if hue is set. If saturation is not set, will assume 100.
|
||||
bool isSetColor() const;
|
||||
ParsedColor getColor() const;
|
||||
|
||||
void load(Stream& stream);
|
||||
void dump(Stream& stream) const;
|
||||
|
||||
void debugState(char const *debugMessage) const;
|
||||
|
||||
static const GroupState& defaultState(MiLightRemoteType remoteType);
|
||||
static GroupState initDefaultRgbState();
|
||||
static GroupState initDefaultWhiteState();
|
||||
static bool isPhysicalField(GroupStateField field);
|
||||
|
||||
private:
|
||||
static const size_t DATA_LONGS = 2;
|
||||
union StateData {
|
||||
uint32_t rawData[DATA_LONGS];
|
||||
struct Fields {
|
||||
uint32_t
|
||||
_state : 1,
|
||||
_brightness : 7,
|
||||
_hue : 8,
|
||||
_saturation : 7,
|
||||
_mode : 4,
|
||||
_bulbMode : 3,
|
||||
_isSetState : 1,
|
||||
_isSetHue : 1;
|
||||
uint32_t
|
||||
_kelvin : 7,
|
||||
_isSetBrightness : 1,
|
||||
_isSetSaturation : 1,
|
||||
_isSetMode : 1,
|
||||
_isSetKelvin : 1,
|
||||
_isSetBulbMode : 1,
|
||||
_brightnessColor : 7,
|
||||
_brightnessMode : 7,
|
||||
_isSetBrightnessColor : 1,
|
||||
_isSetBrightnessMode : 1,
|
||||
_dirty : 1,
|
||||
_mqttDirty : 1,
|
||||
_isSetNightMode : 1,
|
||||
_isNightMode : 1;
|
||||
} fields;
|
||||
};
|
||||
|
||||
// Transient scratchpad that is never persisted. Used to track and compute state for
|
||||
// protocols that only have increment commands (like CCT).
|
||||
union TransientData {
|
||||
uint16_t rawData;
|
||||
struct Fields {
|
||||
uint16_t
|
||||
_isSetKelvinScratch : 1,
|
||||
_kelvinScratch : 7,
|
||||
_isSetBrightnessScratch : 1,
|
||||
_brightnessScratch : 8;
|
||||
} fields;
|
||||
};
|
||||
|
||||
StateData state;
|
||||
TransientData scratchpad;
|
||||
|
||||
// State is constructed from individual command packets. A command packet is parsed in
|
||||
// isolation, and the result is patched onto previous state. There are a few cases where
|
||||
// it's necessary to know some things from the previous state, so we keep a reference to
|
||||
// it here.
|
||||
const GroupState* previousState;
|
||||
|
||||
void applyColor(JsonObject state, uint8_t r, uint8_t g, uint8_t b) const;
|
||||
void applyColor(JsonObject state) const;
|
||||
// Apply OpenHAB-style color, e.g., {"color":"0,0,0"}
|
||||
void applyOhColor(JsonObject state) const;
|
||||
// Apply hex color, e.g., {"color":"#FF0000"}
|
||||
void applyHexColor(JsonObject state) const;
|
||||
};
|
||||
|
||||
extern const BulbId DEFAULT_BULB_ID;
|
||||
|
||||
#endif
|
72
lib/MiLightState/GroupStateCache.cpp
Normal file
72
lib/MiLightState/GroupStateCache.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#include <GroupStateCache.h>
|
||||
|
||||
GroupStateCache::GroupStateCache(const size_t maxSize)
|
||||
: maxSize(maxSize)
|
||||
{ }
|
||||
|
||||
GroupStateCache::~GroupStateCache() {
|
||||
ListNode<GroupCacheNode*>* cur = cache.getHead();
|
||||
|
||||
while (cur != NULL) {
|
||||
delete cur->data;
|
||||
cur = cur->next;
|
||||
}
|
||||
}
|
||||
|
||||
GroupState* GroupStateCache::get(const BulbId& id) {
|
||||
return getInternal(id);
|
||||
}
|
||||
|
||||
GroupState* GroupStateCache::set(const BulbId& id, const GroupState& state) {
|
||||
GroupCacheNode* pushedNode = NULL;
|
||||
if (cache.size() >= maxSize) {
|
||||
pushedNode = cache.pop();
|
||||
}
|
||||
|
||||
GroupState* cachedState = getInternal(id);
|
||||
|
||||
if (cachedState == NULL) {
|
||||
if (pushedNode == NULL) {
|
||||
GroupCacheNode* newNode = new GroupCacheNode(id, state);
|
||||
cachedState = &newNode->state;
|
||||
cache.unshift(newNode);
|
||||
} else {
|
||||
pushedNode->id = id;
|
||||
pushedNode->state = state;
|
||||
cachedState = &pushedNode->state;
|
||||
cache.unshift(pushedNode);
|
||||
}
|
||||
} else {
|
||||
*cachedState = state;
|
||||
}
|
||||
|
||||
return cachedState;
|
||||
}
|
||||
|
||||
BulbId GroupStateCache::getLru() {
|
||||
GroupCacheNode* node = cache.getLast();
|
||||
return node->id;
|
||||
}
|
||||
|
||||
bool GroupStateCache::isFull() const {
|
||||
return cache.size() >= maxSize;
|
||||
}
|
||||
|
||||
ListNode<GroupCacheNode*>* GroupStateCache::getHead() {
|
||||
return cache.getHead();
|
||||
}
|
||||
|
||||
GroupState* GroupStateCache::getInternal(const BulbId& id) {
|
||||
ListNode<GroupCacheNode*>* cur = cache.getHead();
|
||||
|
||||
while (cur != NULL) {
|
||||
if (cur->data->id == id) {
|
||||
GroupState* result = &cur->data->state;
|
||||
cache.spliceToFront(cur);
|
||||
return result;
|
||||
}
|
||||
cur = cur->next;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
34
lib/MiLightState/GroupStateCache.h
Normal file
34
lib/MiLightState/GroupStateCache.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
#include <GroupState.h>
|
||||
#include <LinkedList.h>
|
||||
|
||||
#ifndef _GROUP_STATE_CACHE_H
|
||||
#define _GROUP_STATE_CACHE_H
|
||||
|
||||
struct GroupCacheNode {
|
||||
GroupCacheNode() {}
|
||||
GroupCacheNode(const BulbId& id, const GroupState& state)
|
||||
: id(id), state(state) { }
|
||||
|
||||
BulbId id;
|
||||
GroupState state;
|
||||
};
|
||||
|
||||
class GroupStateCache {
|
||||
public:
|
||||
GroupStateCache(const size_t maxSize);
|
||||
~GroupStateCache();
|
||||
|
||||
GroupState* get(const BulbId& id);
|
||||
GroupState* set(const BulbId& id, const GroupState& state);
|
||||
BulbId getLru();
|
||||
bool isFull() const;
|
||||
ListNode<GroupCacheNode*>* getHead();
|
||||
|
||||
private:
|
||||
LinkedList<GroupCacheNode*> cache;
|
||||
const size_t maxSize;
|
||||
|
||||
GroupState* getInternal(const BulbId& id);
|
||||
};
|
||||
|
||||
#endif
|
40
lib/MiLightState/GroupStatePersistence.cpp
Normal file
40
lib/MiLightState/GroupStatePersistence.cpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#include <GroupStatePersistence.h>
|
||||
#include <FS.h>
|
||||
|
||||
static const char FILE_PREFIX[] = "group_states/";
|
||||
|
||||
void GroupStatePersistence::get(const BulbId &id, GroupState& state) {
|
||||
char path[30];
|
||||
memset(path, 0, 30);
|
||||
buildFilename(id, path);
|
||||
|
||||
if (SPIFFS.exists(path)) {
|
||||
File f = SPIFFS.open(path, "r");
|
||||
state.load(f);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupStatePersistence::set(const BulbId &id, const GroupState& state) {
|
||||
char path[30];
|
||||
memset(path, 0, 30);
|
||||
buildFilename(id, path);
|
||||
|
||||
File f = SPIFFS.open(path, "w");
|
||||
state.dump(f);
|
||||
f.close();
|
||||
}
|
||||
|
||||
void GroupStatePersistence::clear(const BulbId &id) {
|
||||
char path[30];
|
||||
buildFilename(id, path);
|
||||
|
||||
if (SPIFFS.exists(path)) {
|
||||
SPIFFS.remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
char* GroupStatePersistence::buildFilename(const BulbId &id, char *buffer) {
|
||||
uint32_t compactId = id.getCompactId();
|
||||
return buffer + sprintf(buffer, "%s%x", FILE_PREFIX, compactId);
|
||||
}
|
18
lib/MiLightState/GroupStatePersistence.h
Normal file
18
lib/MiLightState/GroupStatePersistence.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
#include <GroupState.h>
|
||||
|
||||
#ifndef _GROUP_STATE_PERSISTENCE_H
|
||||
#define _GROUP_STATE_PERSISTENCE_H
|
||||
|
||||
class GroupStatePersistence {
|
||||
public:
|
||||
void get(const BulbId& id, GroupState& state);
|
||||
void set(const BulbId& id, const GroupState& state);
|
||||
|
||||
void clear(const BulbId& id);
|
||||
|
||||
private:
|
||||
|
||||
static char* buildFilename(const BulbId& id, char* buffer);
|
||||
};
|
||||
|
||||
#endif
|
150
lib/MiLightState/GroupStateStore.cpp
Normal file
150
lib/MiLightState/GroupStateStore.cpp
Normal file
|
@ -0,0 +1,150 @@
|
|||
#include <GroupStateStore.h>
|
||||
#include <MiLightRemoteConfig.h>
|
||||
|
||||
GroupStateStore::GroupStateStore(const size_t maxSize, const size_t flushRate)
|
||||
: cache(GroupStateCache(maxSize)),
|
||||
flushRate(flushRate),
|
||||
lastFlush(0)
|
||||
{ }
|
||||
|
||||
GroupState* GroupStateStore::get(const BulbId& id) {
|
||||
GroupState* state = cache.get(id);
|
||||
|
||||
if (state == NULL) {
|
||||
#if STATE_DEBUG
|
||||
printf(
|
||||
"Couldn't fetch state for 0x%04X / %d / %s in the cache, getting it from persistence\n",
|
||||
id.deviceId,
|
||||
id.groupId,
|
||||
MiLightRemoteConfig::fromType(id.deviceType)->name.c_str()
|
||||
);
|
||||
#endif
|
||||
trackEviction();
|
||||
GroupState loadedState = GroupState::defaultState(id.deviceType);
|
||||
|
||||
const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromType(id.deviceType);
|
||||
|
||||
if (remoteConfig == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
persistence.get(id, loadedState);
|
||||
state = cache.set(id, loadedState);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
GroupState* GroupStateStore::get(const uint16_t deviceId, const uint8_t groupId, const MiLightRemoteType deviceType) {
|
||||
BulbId bulbId(deviceId, groupId, deviceType);
|
||||
return get(bulbId);
|
||||
}
|
||||
|
||||
// Save state for a bulb.
|
||||
//
|
||||
// Notes:
|
||||
//
|
||||
// * For device types with groups, group 0 is a "virtual" group. All devices paired with the same ID will
|
||||
// respond to group 0. When state for an individual (i.e., != 0) group is changed, the state for
|
||||
// group 0 becomes out of sync and should be cleared.
|
||||
//
|
||||
// * If id.groupId == 0, will iterate across all groups and individually save each group (recursively)
|
||||
//
|
||||
GroupState* GroupStateStore::set(const BulbId &id, const GroupState& state) {
|
||||
BulbId otherId(id);
|
||||
GroupState* storedState = get(id);
|
||||
storedState->patch(state);
|
||||
|
||||
if (id.groupId == 0) {
|
||||
const MiLightRemoteConfig* remote = MiLightRemoteConfig::fromType(id.deviceType);
|
||||
|
||||
#ifdef STATE_DEBUG
|
||||
Serial.printf_P(PSTR("Fanning out group 0 state for device ID 0x%04X (%d groups in total)\n"), id.deviceId, remote->numGroups);
|
||||
state.debugState("group 0 state = ");
|
||||
#endif
|
||||
|
||||
for (size_t i = 1; i <= remote->numGroups; i++) {
|
||||
otherId.groupId = i;
|
||||
|
||||
GroupState* individualState = get(otherId);
|
||||
individualState->patch(state);
|
||||
}
|
||||
} else {
|
||||
otherId.groupId = 0;
|
||||
GroupState* group0State = get(otherId);
|
||||
|
||||
group0State->clearNonMatchingFields(state);
|
||||
}
|
||||
|
||||
return storedState;
|
||||
}
|
||||
|
||||
GroupState* GroupStateStore::set(const uint16_t deviceId, const uint8_t groupId, const MiLightRemoteType deviceType, const GroupState& state) {
|
||||
BulbId bulbId(deviceId, groupId, deviceType);
|
||||
return set(bulbId, state);
|
||||
}
|
||||
|
||||
void GroupStateStore::clear(const BulbId& bulbId) {
|
||||
GroupState* state = get(bulbId);
|
||||
|
||||
if (state != NULL) {
|
||||
state->initFields();
|
||||
state->patch(GroupState::defaultState(bulbId.deviceType));
|
||||
}
|
||||
}
|
||||
|
||||
void GroupStateStore::trackEviction() {
|
||||
if (cache.isFull()) {
|
||||
evictedIds.add(cache.getLru());
|
||||
|
||||
#ifdef STATE_DEBUG
|
||||
BulbId bulbId = evictedIds.getLast();
|
||||
printf(
|
||||
"Evicting from cache: 0x%04X / %d / %s\n",
|
||||
bulbId.deviceId,
|
||||
bulbId.groupId,
|
||||
MiLightRemoteConfig::fromType(bulbId.deviceType)->name.c_str()
|
||||
);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool GroupStateStore::flush() {
|
||||
ListNode<GroupCacheNode*>* curr = cache.getHead();
|
||||
bool anythingFlushed = false;
|
||||
|
||||
while (curr != NULL && curr->data->state.isDirty() && !anythingFlushed) {
|
||||
persistence.set(curr->data->id, curr->data->state);
|
||||
curr->data->state.clearDirty();
|
||||
|
||||
#ifdef STATE_DEBUG
|
||||
BulbId bulbId = curr->data->id;
|
||||
printf(
|
||||
"Flushing dirty state for 0x%04X / %d / %s\n",
|
||||
bulbId.deviceId,
|
||||
bulbId.groupId,
|
||||
MiLightRemoteConfig::fromType(bulbId.deviceType)->name.c_str()
|
||||
);
|
||||
#endif
|
||||
|
||||
curr = curr->next;
|
||||
anythingFlushed = true;
|
||||
}
|
||||
|
||||
while (evictedIds.size() > 0 && !anythingFlushed) {
|
||||
persistence.clear(evictedIds.shift());
|
||||
anythingFlushed = true;
|
||||
}
|
||||
|
||||
return anythingFlushed;
|
||||
}
|
||||
|
||||
void GroupStateStore::limitedFlush() {
|
||||
unsigned long now = millis();
|
||||
|
||||
if ((lastFlush + flushRate) < now) {
|
||||
if (flush()) {
|
||||
lastFlush = now;
|
||||
}
|
||||
}
|
||||
}
|
53
lib/MiLightState/GroupStateStore.h
Normal file
53
lib/MiLightState/GroupStateStore.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
#include <GroupState.h>
|
||||
#include <GroupStateCache.h>
|
||||
#include <GroupStatePersistence.h>
|
||||
|
||||
#ifndef _GROUP_STATE_STORE_H
|
||||
#define _GROUP_STATE_STORE_H
|
||||
|
||||
class GroupStateStore {
|
||||
public:
|
||||
GroupStateStore(const size_t maxSize, const size_t flushRate);
|
||||
|
||||
/*
|
||||
* Returns the state for the given BulbId. If accessing state for a valid device
|
||||
* (i.e., NOT group 0) and no state exists, its state will be initialized with a
|
||||
* default.
|
||||
*
|
||||
* Otherwise, we return NULL.
|
||||
*/
|
||||
GroupState* get(const BulbId& id);
|
||||
GroupState* get(const uint16_t deviceId, const uint8_t groupId, const MiLightRemoteType deviceType);
|
||||
|
||||
/*
|
||||
* Sets the state for the given BulbId. State will be marked as dirty and
|
||||
* flushed to persistent storage.
|
||||
*/
|
||||
GroupState* set(const BulbId& id, const GroupState& state);
|
||||
GroupState* set(const uint16_t deviceId, const uint8_t groupId, const MiLightRemoteType deviceType, const GroupState& state);
|
||||
|
||||
void clear(const BulbId& id);
|
||||
|
||||
/*
|
||||
* Flushes all states to persistent storage. Returns true iff anything was
|
||||
* flushed.
|
||||
*/
|
||||
bool flush();
|
||||
|
||||
/*
|
||||
* Flushes at most one dirty state to persistent storage. Rate limit
|
||||
* specified by Settings.
|
||||
*/
|
||||
void limitedFlush();
|
||||
|
||||
private:
|
||||
GroupStateCache cache;
|
||||
GroupStatePersistence persistence;
|
||||
LinkedList<BulbId> evictedIds;
|
||||
const size_t flushRate;
|
||||
unsigned long lastFlush;
|
||||
|
||||
void trackEviction();
|
||||
};
|
||||
|
||||
#endif
|
451
lib/Radio/LT8900MiLightRadio.cpp
Normal file
451
lib/Radio/LT8900MiLightRadio.cpp
Normal file
|
@ -0,0 +1,451 @@
|
|||
/*
|
||||
* MiLightRadioPL1167_LT89000.cpp
|
||||
*
|
||||
* Created on: 31 March 2017
|
||||
* Author: WoodsterDK
|
||||
*
|
||||
* Very inspired by:
|
||||
* https://github.com/pmoscetta/authometion-milight/tree/master/Authometion-MiLight
|
||||
* https://bitbucket.org/robvanderveer/lt8900lib
|
||||
*/
|
||||
|
||||
#include "LT8900MiLightRadio.h"
|
||||
#include <SPI.h>
|
||||
|
||||
/**************************************************************************/
|
||||
// Constructor
|
||||
/**************************************************************************/
|
||||
LT8900MiLightRadio::LT8900MiLightRadio(byte byCSPin, byte byResetPin, byte byPktFlag, const MiLightRadioConfig& config)
|
||||
: _config(config),
|
||||
_channel(0),
|
||||
_currentPacketLen(0),
|
||||
_currentPacketPos(0)
|
||||
{
|
||||
_csPin = byCSPin;
|
||||
_pin_pktflag = byPktFlag;
|
||||
|
||||
pinMode(_pin_pktflag, INPUT);
|
||||
|
||||
if (byResetPin > 0) // If zero then bypass hardware reset
|
||||
{
|
||||
pinMode(byResetPin, OUTPUT);
|
||||
digitalWrite(byResetPin, LOW);
|
||||
delay(200);
|
||||
digitalWrite(byResetPin, HIGH);
|
||||
delay(200);
|
||||
}
|
||||
|
||||
pinMode(_csPin, OUTPUT);
|
||||
digitalWrite(_csPin, HIGH);
|
||||
|
||||
SPI.begin();
|
||||
|
||||
SPI.setDataMode(SPI_MODE1);
|
||||
// The following speed settings depends upon the wiring and PCB
|
||||
//SPI.setFrequency(8000000);
|
||||
SPI.setFrequency(4000000);
|
||||
SPI.setBitOrder(MSBFIRST);
|
||||
|
||||
//Initialize transceiver with correct settings
|
||||
vInitRadioModule();
|
||||
delay(50);
|
||||
|
||||
// Check if HW is connected
|
||||
_bConnected = bCheckRadioConnection();
|
||||
|
||||
//Reset SPI MODE to default
|
||||
SPI.setDataMode(SPI_MODE0);
|
||||
_waiting = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**************************************************************************/
|
||||
// Checks the connection to the radio module by verifying a register setting
|
||||
/**************************************************************************/
|
||||
bool LT8900MiLightRadio::bCheckRadioConnection(void)
|
||||
{
|
||||
bool bRetValue = false;
|
||||
uint16_t value_0 = uiReadRegister(0);
|
||||
uint16_t value_1 = uiReadRegister(1);
|
||||
|
||||
if ((value_0 == 0x6fe0) && (value_1 == 0x5681))
|
||||
{
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println(F("Radio module running correctly..."));
|
||||
#endif
|
||||
bRetValue = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println(F("Failed initializing the radio module..."));
|
||||
#endif
|
||||
}
|
||||
|
||||
return bRetValue;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Initialize radio module
|
||||
/**************************************************************************/
|
||||
void LT8900MiLightRadio::vInitRadioModule() {
|
||||
bool bWriteDefaultDefault = true; // Is it okay to use the default power up values, without setting them
|
||||
|
||||
regWrite16(0x00, 0x6F, 0xE0, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x02, 0x66, 0x17, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x04, 0x9C, 0xC9, 7); // Recommended value by PMmicro
|
||||
|
||||
regWrite16(0x05, 0x66, 0x37, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x07, 0x00, 0x4C, 7); // PL1167's TX/RX Enable and Channel Register, Default channel 76
|
||||
regWrite16(0x08, 0x6C, 0x90, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x09, 0x48, 0x00, 7); // PA Control register
|
||||
|
||||
regWrite16(0x0B, 0x00, 0x08, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x0D, 0x48, 0xBD, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x16, 0x00, 0xFF, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x18, 0x00, 0x67, 7); // Recommended value by PMmicro
|
||||
|
||||
regWrite16(0x1A, 0x19, 0xE0, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x1B, 0x13, 0x00, 7); // Recommended value by PMmicro
|
||||
|
||||
regWrite16(0x20, 0x48, 0x00, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x21, 0x3F, 0xC7, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x22, 0x20, 0x00, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x23, 0x03, 0x00, 7); // Recommended value by PMmicro
|
||||
|
||||
regWrite16(0x24, 0x72, 0x36, 7); // Sync R0
|
||||
regWrite16(0x27, 0x18, 0x09, 7); // Sync R3
|
||||
regWrite16(0x28, 0x44, 0x02, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x29, 0xB0, 0x00, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x2A, 0xFD, 0xB0, 7); // Recommended value by PMmicro
|
||||
|
||||
if (bWriteDefaultDefault == true) {
|
||||
regWrite16(0x01, 0x56, 0x81, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x0A, 0x7F, 0xFD, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x0C, 0x00, 0x00, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x17, 0x80, 0x05, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x19, 0x16, 0x59, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x1C, 0x18, 0x00, 7); // Recommended value by PMmicro
|
||||
|
||||
regWrite16(0x25, 0x00, 0x00, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x26, 0x00, 0x00, 7); // Recommended value by PMmicro
|
||||
regWrite16(0x2B, 0x00, 0x0F, 7); // Recommended value by PMmicro
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Set sync word
|
||||
/**************************************************************************/
|
||||
void LT8900MiLightRadio::vSetSyncWord(uint16_t syncWord3, uint16_t syncWord2, uint16_t syncWord1, uint16_t syncWord0)
|
||||
{
|
||||
uiWriteRegister(R_SYNCWORD1, syncWord0);
|
||||
uiWriteRegister(R_SYNCWORD2, syncWord1);
|
||||
uiWriteRegister(R_SYNCWORD3, syncWord1);
|
||||
uiWriteRegister(R_SYNCWORD4, syncWord3);
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Low level register write with delay
|
||||
/**************************************************************************/
|
||||
void LT8900MiLightRadio::regWrite16(byte ADDR, byte V1, byte V2, byte WAIT)
|
||||
{
|
||||
digitalWrite(_csPin, LOW);
|
||||
SPI.transfer(ADDR);
|
||||
SPI.transfer(V1);
|
||||
SPI.transfer(V2);
|
||||
digitalWrite(_csPin, HIGH);
|
||||
delayMicroseconds(WAIT);
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************/
|
||||
// Low level register read
|
||||
/**************************************************************************/
|
||||
uint16_t LT8900MiLightRadio::uiReadRegister(uint8_t reg)
|
||||
{
|
||||
SPI.setDataMode(SPI_MODE1);
|
||||
digitalWrite(_csPin, LOW);
|
||||
SPI.transfer(REGISTER_READ | (REGISTER_MASK & reg));
|
||||
uint8_t high = SPI.transfer(0x00);
|
||||
uint8_t low = SPI.transfer(0x00);
|
||||
|
||||
digitalWrite(_csPin, HIGH);
|
||||
|
||||
SPI.setDataMode(SPI_MODE0);
|
||||
return (high << 8 | low);
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************/
|
||||
// Low level 16bit register write
|
||||
/**************************************************************************/
|
||||
uint8_t LT8900MiLightRadio::uiWriteRegister(uint8_t reg, uint16_t data)
|
||||
{
|
||||
uint8_t high = data >> 8;
|
||||
uint8_t low = data & 0xFF;
|
||||
|
||||
digitalWrite(_csPin, LOW);
|
||||
|
||||
uint8_t result = SPI.transfer(REGISTER_WRITE | (REGISTER_MASK & reg));
|
||||
SPI.transfer(high);
|
||||
SPI.transfer(low);
|
||||
|
||||
digitalWrite(_csPin, HIGH);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Start listening on specified channel and syncword
|
||||
/**************************************************************************/
|
||||
void LT8900MiLightRadio::vStartListening(uint uiChannelToListenTo)
|
||||
{
|
||||
_dupes_received = 0;
|
||||
vSetSyncWord(_config.syncword3, 0,0,_config.syncword0);
|
||||
//vSetChannel(uiChannelToListenTo);
|
||||
|
||||
_channel = uiChannelToListenTo;
|
||||
|
||||
vResumeRX();
|
||||
delay(5);
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Resume listening - without changing the channel and syncword
|
||||
/**************************************************************************/
|
||||
void LT8900MiLightRadio::vResumeRX(void)
|
||||
{
|
||||
_dupes_received = 0;
|
||||
uiWriteRegister(R_CHANNEL, _channel & CHANNEL_MASK); //turn off rx/tx
|
||||
delay(3);
|
||||
uiWriteRegister(R_FIFO_CONTROL, 0x0080); //flush rx
|
||||
uiWriteRegister(R_CHANNEL, (_channel & CHANNEL_MASK) | _BV(CHANNEL_RX_BIT)); //enable RX
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Check if data is available using the hardware pin PKT_FLAG
|
||||
/**************************************************************************/
|
||||
bool LT8900MiLightRadio::bAvailablePin() {
|
||||
return digitalRead(_pin_pktflag) > 0;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Check if data is available using the PKT_FLAG state in the status register
|
||||
/**************************************************************************/
|
||||
bool LT8900MiLightRadio::bAvailableRegister() {
|
||||
//read the PKT_FLAG state; this can also be done with a hard wire.
|
||||
uint16_t value = uiReadRegister(R_STATUS);
|
||||
|
||||
if (bitRead(value, STATUS_CRC_BIT) != 0) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println(F("LT8900: CRC failed"));
|
||||
#endif
|
||||
vResumeRX();
|
||||
return false;
|
||||
}
|
||||
|
||||
return (value & STATUS_PKT_BIT_MASK) > 0;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Read the RX buffer
|
||||
/**************************************************************************/
|
||||
int LT8900MiLightRadio::iReadRXBuffer(uint8_t *buffer, size_t maxBuffer) {
|
||||
size_t bufferIx = 0;
|
||||
uint16_t data;
|
||||
|
||||
if (_currentPacketLen == 0) {
|
||||
if (! available()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
data = uiReadRegister(R_FIFO);
|
||||
|
||||
_currentPacketLen = (data >> 8);
|
||||
_currentPacketPos = 1;
|
||||
|
||||
buffer[bufferIx++] = (data & 0xFF);
|
||||
}
|
||||
|
||||
while (_currentPacketPos < _currentPacketLen && (bufferIx+1) < maxBuffer) {
|
||||
data = uiReadRegister(R_FIFO);
|
||||
buffer[bufferIx++] = data >> 8;
|
||||
buffer[bufferIx++] = data & 0xFF;
|
||||
|
||||
_currentPacketPos += 2;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
printf_P(
|
||||
PSTR("Read %d/%d bytes in RX, read %d bytes into buffer\n"),
|
||||
_currentPacketPos,
|
||||
_currentPacketLen,
|
||||
bufferIx
|
||||
);
|
||||
#endif
|
||||
|
||||
if (_currentPacketPos >= _currentPacketLen) {
|
||||
_currentPacketPos = 0;
|
||||
_currentPacketLen = 0;
|
||||
}
|
||||
|
||||
return bufferIx;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************/
|
||||
// Set the active channel for the radio module
|
||||
/**************************************************************************/
|
||||
void LT8900MiLightRadio::vSetChannel(uint8_t channel)
|
||||
{
|
||||
_channel = channel;
|
||||
uiWriteRegister(R_CHANNEL, (_channel & CHANNEL_MASK));
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Startup
|
||||
/**************************************************************************/
|
||||
int LT8900MiLightRadio::begin()
|
||||
{
|
||||
vSetChannel(_config.channels[0]);
|
||||
configure();
|
||||
available();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Configure the module according to type, and start listening
|
||||
/**************************************************************************/
|
||||
int LT8900MiLightRadio::configure()
|
||||
{
|
||||
vInitRadioModule();
|
||||
vSetSyncWord(_config.syncword3, 0,0,_config.syncword0);
|
||||
vStartListening(_config.channels[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Check if data is available
|
||||
/**************************************************************************/
|
||||
bool LT8900MiLightRadio::available()
|
||||
{
|
||||
if (_currentPacketPos < _currentPacketLen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return bAvailablePin() && bAvailableRegister();
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Read received data from buffer to upper layer
|
||||
/**************************************************************************/
|
||||
int LT8900MiLightRadio::read(uint8_t frame[], size_t &frame_length)
|
||||
{
|
||||
if (!available()) {
|
||||
frame_length = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println(F("LT8900: Radio was available, reading packet..."));
|
||||
#endif
|
||||
|
||||
uint8_t buf[MILIGHT_MAX_PACKET_LENGTH];
|
||||
int packetSize = iReadRXBuffer(buf, MILIGHT_MAX_PACKET_LENGTH);
|
||||
|
||||
if (packetSize > 0) {
|
||||
frame_length = packetSize;
|
||||
memcpy(frame, buf, packetSize);
|
||||
}
|
||||
|
||||
vResumeRX();
|
||||
|
||||
return packetSize;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Write data
|
||||
/**************************************************************************/
|
||||
int LT8900MiLightRadio::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;
|
||||
|
||||
SPI.setDataMode(SPI_MODE1);
|
||||
|
||||
int retval = resend();
|
||||
yield();
|
||||
|
||||
SPI.setDataMode(SPI_MODE0);
|
||||
|
||||
if (retval < 0) {
|
||||
return retval;
|
||||
}
|
||||
return frame_length;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// Handle the transmission to regarding to freq diversity and repeats
|
||||
/**************************************************************************/
|
||||
int LT8900MiLightRadio::resend()
|
||||
{
|
||||
byte Length = _out_packet[0];
|
||||
|
||||
for (size_t i = 0; i < MiLightRadioConfig::NUM_CHANNELS; i++)
|
||||
{
|
||||
sendPacket(_out_packet, Length, _config.channels[i]);
|
||||
delayMicroseconds(DEFAULT_TIME_BETWEEN_RETRANSMISSIONS_uS);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
// The actual transmit happens here
|
||||
/**************************************************************************/
|
||||
bool LT8900MiLightRadio::sendPacket(uint8_t *data, size_t packetSize, byte byChannel)
|
||||
{
|
||||
if(_bConnected) // Must be connected to module otherwise it might lookup waiting for _pin_pktflag
|
||||
{
|
||||
if (packetSize < 1 || packetSize > 255)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uiWriteRegister(R_CHANNEL, 0x0000);
|
||||
uiWriteRegister(R_FIFO_CONTROL, 0x8080); //flush tx and RX
|
||||
|
||||
digitalWrite(_csPin, LOW); // Enable PL1167 SPI transmission
|
||||
SPI.transfer(R_FIFO); // Start writing PL1167's FIFO Data register
|
||||
SPI.transfer(packetSize); // Length of data buffer: x bytes
|
||||
|
||||
for (byte iCounter = 0; iCounter < packetSize; iCounter++)
|
||||
{
|
||||
SPI.transfer((data[1+iCounter]));
|
||||
}
|
||||
digitalWrite(_csPin, HIGH); // Disable PL1167 SPI transmission
|
||||
delayMicroseconds(10);
|
||||
|
||||
uiWriteRegister(R_CHANNEL, (byChannel & CHANNEL_MASK) | _BV(CHANNEL_TX_BIT)); //enable RX
|
||||
|
||||
//Wait until the packet is sent.
|
||||
while (digitalRead(_pin_pktflag) == 0)
|
||||
{
|
||||
//do nothing.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const MiLightRadioConfig& LT8900MiLightRadio::config() {
|
||||
return _config;
|
||||
}
|
90
lib/Radio/LT8900MiLightRadio.h
Normal file
90
lib/Radio/LT8900MiLightRadio.h
Normal file
|
@ -0,0 +1,90 @@
|
|||
#ifdef ARDUINO
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#endif
|
||||
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <MiLightRadio.h>
|
||||
|
||||
//#define DEBUG_PRINTF
|
||||
|
||||
// Register defines
|
||||
#define REGISTER_READ 0b10000000 //bin
|
||||
#define REGISTER_WRITE 0b00000000 //bin
|
||||
#define REGISTER_MASK 0b01111111 //bin
|
||||
|
||||
#define R_CHANNEL 7
|
||||
#define CHANNEL_RX_BIT 7
|
||||
#define CHANNEL_TX_BIT 8
|
||||
#define CHANNEL_MASK 0b01111111 ///bin
|
||||
|
||||
#define STATUS_PKT_BIT_MASK 0x40
|
||||
|
||||
#define R_STATUS 48
|
||||
#define STATUS_CRC_BIT 15
|
||||
|
||||
#define R_FIFO 50
|
||||
#define R_FIFO_CONTROL 52
|
||||
|
||||
#define R_SYNCWORD1 36
|
||||
#define R_SYNCWORD2 37
|
||||
#define R_SYNCWORD3 38
|
||||
#define R_SYNCWORD4 39
|
||||
|
||||
#define DEFAULT_TIME_BETWEEN_RETRANSMISSIONS_uS 350
|
||||
// #define DEFAULT_TIME_BETWEEN_RETRANSMISSIONS_uS 0
|
||||
|
||||
#ifndef MILIGHTRADIOPL1167_LT8900_H_
|
||||
#define MILIGHTRADIOPL1167_LT8900_H_
|
||||
|
||||
class LT8900MiLightRadio : public MiLightRadio {
|
||||
public:
|
||||
LT8900MiLightRadio(byte byCSPin, byte byResetPin, byte byPktFlag, const MiLightRadioConfig& config);
|
||||
|
||||
virtual int begin();
|
||||
virtual bool available();
|
||||
virtual int read(uint8_t frame[], size_t &frame_length);
|
||||
virtual int write(uint8_t frame[], size_t frame_length);
|
||||
virtual int resend();
|
||||
virtual int configure();
|
||||
virtual const MiLightRadioConfig& config();
|
||||
|
||||
private:
|
||||
|
||||
void vInitRadioModule();
|
||||
void vSetSyncWord(uint16_t syncWord3, uint16_t syncWord2, uint16_t syncWord1, uint16_t syncWord0);
|
||||
uint16_t uiReadRegister(uint8_t reg);
|
||||
void regWrite16(byte ADDR, byte V1, byte V2, byte WAIT);
|
||||
uint8_t uiWriteRegister(uint8_t reg, uint16_t data);
|
||||
|
||||
bool bAvailablePin(void);
|
||||
bool bAvailableRegister(void);
|
||||
void vStartListening(uint uiChannelToListenTo);
|
||||
void vResumeRX(void);
|
||||
int iReadRXBuffer(uint8_t *buffer, size_t maxBuffer);
|
||||
void vSetChannel(uint8_t channel);
|
||||
void vGenericSendPacket(int iMode, int iLength, byte *pbyFrame, byte byChannel );
|
||||
bool bCheckRadioConnection(void);
|
||||
bool sendPacket(uint8_t *data, size_t packetSize,byte byChannel);
|
||||
|
||||
byte _pin_pktflag;
|
||||
byte _csPin;
|
||||
bool _bConnected;
|
||||
|
||||
const MiLightRadioConfig& _config;
|
||||
|
||||
uint8_t _channel;
|
||||
uint8_t _packet[10];
|
||||
uint8_t _out_packet[10];
|
||||
bool _waiting;
|
||||
int _dupes_received;
|
||||
size_t _currentPacketLen;
|
||||
size_t _currentPacketPos;
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif
|
30
lib/Radio/MiLightRadio.h
Normal file
30
lib/Radio/MiLightRadio.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
#ifdef ARDUINO
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
#include <MiLightRadioConfig.h>
|
||||
|
||||
#ifndef _MILIGHT_RADIO_H_
|
||||
#define _MILIGHT_RADIO_H_
|
||||
|
||||
class MiLightRadio {
|
||||
public:
|
||||
|
||||
virtual int begin();
|
||||
virtual bool available();
|
||||
virtual int read(uint8_t frame[], size_t &frame_length);
|
||||
virtual int write(uint8_t frame[], size_t frame_length);
|
||||
virtual int resend();
|
||||
virtual int configure();
|
||||
virtual const MiLightRadioConfig& config();
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
#endif
|
9
lib/Radio/MiLightRadioConfig.cpp
Normal file
9
lib/Radio/MiLightRadioConfig.cpp
Normal file
|
@ -0,0 +1,9 @@
|
|||
#include <MiLightRadioConfig.h>
|
||||
|
||||
MiLightRadioConfig MiLightRadioConfig::ALL_CONFIGS[] = {
|
||||
MiLightRadioConfig(0x147A, 0x258B, 7, 9, 40, 71, 0xAA, 0x05), // rgbw
|
||||
MiLightRadioConfig(0x050A, 0x55AA, 7, 4, 39, 74, 0xAA, 0x05), // cct
|
||||
MiLightRadioConfig(0x7236, 0x1809, 9, 8, 39, 70, 0xAA, 0x05), // rgb+cct, fut089
|
||||
MiLightRadioConfig(0x9AAB, 0xBCCD, 6, 3, 38, 73, 0x55, 0x0A), // rgb
|
||||
MiLightRadioConfig(0x50A0, 0xAA55, 6, 6, 41, 76, 0xAA, 0x0A) // FUT020
|
||||
};
|
83
lib/Radio/MiLightRadioConfig.h
Normal file
83
lib/Radio/MiLightRadioConfig.h
Normal file
|
@ -0,0 +1,83 @@
|
|||
#include <Arduino.h>
|
||||
#include <MiLightRemoteType.h>
|
||||
#include <Size.h>
|
||||
#include <RadioUtils.h>
|
||||
|
||||
#ifndef _MILIGHT_RADIO_CONFIG
|
||||
#define _MILIGHT_RADIO_CONFIG
|
||||
|
||||
#define MILIGHT_MAX_PACKET_LENGTH 9
|
||||
|
||||
class MiLightRadioConfig {
|
||||
public:
|
||||
static const size_t NUM_CHANNELS = 3;
|
||||
|
||||
// We can set this to two possible values. It only has an affect on the nRF24 radio. The
|
||||
// LT8900/PL1167 radio will always use the raw syncwords. For the nRF24, this controls what
|
||||
// we set the "address" to, which roughly corresponds to the LT8900 syncword.
|
||||
//
|
||||
// The PL1167 packet is structured as follows (lengths in bits):
|
||||
// Preamble ( 8) | Syncword (32) | Trailer ( 4) | Packet Len ( 8) | Packet (...)
|
||||
//
|
||||
// 4 -- Use the raw syncword bits as the address. This means the Trailer will be included in
|
||||
// the packet data. Since the Trailer is 4 bits, packet data will not be byte-aligned,
|
||||
// and the data must be bitshifted every time it's received.
|
||||
//
|
||||
// 5 -- Include the Trailer in the syncword. Avoids us needing to bitshift packet data. The
|
||||
// downside is that the Trailer is hardcoded and assumed based on received packets.
|
||||
//
|
||||
// In general, this should be set to 5 unless packets that should be showing up are
|
||||
// mysteriously not present.
|
||||
static const uint8_t SYNCWORD_LENGTH = 5;
|
||||
|
||||
MiLightRadioConfig(
|
||||
const uint16_t syncword0,
|
||||
const uint16_t syncword3,
|
||||
const size_t packetLength,
|
||||
const uint8_t channel0,
|
||||
const uint8_t channel1,
|
||||
const uint8_t channel2,
|
||||
const uint8_t preamble,
|
||||
const uint8_t trailer
|
||||
) : syncword0(syncword0)
|
||||
, syncword3(syncword3)
|
||||
, packetLength(packetLength)
|
||||
{
|
||||
channels[0] = channel0;
|
||||
channels[1] = channel1;
|
||||
channels[2] = channel2;
|
||||
|
||||
size_t ix = SYNCWORD_LENGTH;
|
||||
|
||||
// precompute the syncword for the nRF24. we include the fixed preamble and trailer in the
|
||||
// syncword to avoid needing to bitshift packets. trailer is 4 bits, so the actual syncword
|
||||
// is no longer byte-aligned.
|
||||
if (SYNCWORD_LENGTH == 5) {
|
||||
syncwordBytes[ --ix ] = reverseBits(
|
||||
((syncword0 << 4) & 0xF0) | (preamble & 0x0F)
|
||||
);
|
||||
syncwordBytes[ --ix ] = reverseBits((syncword0 >> 4) & 0xFF);
|
||||
syncwordBytes[ --ix ] = reverseBits(((syncword0 >> 12) & 0x0F) + ((syncword3 << 4) & 0xF0));
|
||||
syncwordBytes[ --ix ] = reverseBits((syncword3 >> 4) & 0xFF);
|
||||
syncwordBytes[ --ix ] = reverseBits(
|
||||
((syncword3 >> 12) & 0x0F) | ((trailer << 4) & 0xF0)
|
||||
);
|
||||
} else {
|
||||
syncwordBytes[ --ix ] = reverseBits(syncword0 & 0xff);
|
||||
syncwordBytes[ --ix ] = reverseBits( (syncword0 >> 8) & 0xff);
|
||||
syncwordBytes[ --ix ] = reverseBits(syncword3 & 0xff);
|
||||
syncwordBytes[ --ix ] = reverseBits( (syncword3 >> 8) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t channels[3];
|
||||
uint8_t syncwordBytes[SYNCWORD_LENGTH];
|
||||
uint16_t syncword0, syncword3;
|
||||
|
||||
const size_t packetLength;
|
||||
|
||||
static const size_t NUM_CONFIGS = 5;
|
||||
static MiLightRadioConfig ALL_CONFIGS[NUM_CONFIGS];
|
||||
};
|
||||
|
||||
#endif
|
48
lib/Radio/MiLightRadioFactory.cpp
Normal file
48
lib/Radio/MiLightRadioFactory.cpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#include <MiLightRadioFactory.h>
|
||||
|
||||
std::shared_ptr<MiLightRadioFactory> MiLightRadioFactory::fromSettings(const Settings& settings) {
|
||||
switch (settings.radioInterfaceType) {
|
||||
case nRF24:
|
||||
return std::make_shared<NRF24Factory>(
|
||||
settings.csnPin,
|
||||
settings.cePin,
|
||||
settings.rf24PowerLevel,
|
||||
settings.rf24Channels,
|
||||
settings.rf24ListenChannel
|
||||
);
|
||||
|
||||
case LT8900:
|
||||
return std::make_shared<LT8900Factory>(settings.csnPin, settings.resetPin, settings.cePin);
|
||||
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
NRF24Factory::NRF24Factory(
|
||||
uint8_t csnPin,
|
||||
uint8_t cePin,
|
||||
RF24PowerLevel rF24PowerLevel,
|
||||
const std::vector<RF24Channel>& channels,
|
||||
RF24Channel listenChannel
|
||||
)
|
||||
: rf24(RF24(cePin, csnPin)),
|
||||
channels(channels),
|
||||
listenChannel(listenChannel)
|
||||
{
|
||||
rf24.setPALevel(RF24PowerLevelHelpers::rf24ValueFromValue(rF24PowerLevel));
|
||||
}
|
||||
|
||||
std::shared_ptr<MiLightRadio> NRF24Factory::create(const MiLightRadioConfig &config) {
|
||||
return std::make_shared<NRF24MiLightRadio>(rf24, config, channels, listenChannel);
|
||||
}
|
||||
|
||||
LT8900Factory::LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag)
|
||||
: _csPin(csPin),
|
||||
_resetPin(resetPin),
|
||||
_pktFlag(pktFlag)
|
||||
{ }
|
||||
|
||||
std::shared_ptr<MiLightRadio> LT8900Factory::create(const MiLightRadioConfig& config) {
|
||||
return std::make_shared<LT8900MiLightRadio>(_csPin, _resetPin, _pktFlag, config);
|
||||
}
|
62
lib/Radio/MiLightRadioFactory.h
Normal file
62
lib/Radio/MiLightRadioFactory.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include <RF24.h>
|
||||
#include <PL1167_nRF24.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <MiLightRadio.h>
|
||||
#include <NRF24MiLightRadio.h>
|
||||
#include <LT8900MiLightRadio.h>
|
||||
#include <RF24PowerLevel.h>
|
||||
#include <RF24Channel.h>
|
||||
#include <Settings.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#ifndef _MILIGHT_RADIO_FACTORY_H
|
||||
#define _MILIGHT_RADIO_FACTORY_H
|
||||
|
||||
class MiLightRadioFactory {
|
||||
public:
|
||||
|
||||
virtual ~MiLightRadioFactory() { };
|
||||
virtual std::shared_ptr<MiLightRadio> create(const MiLightRadioConfig& config) = 0;
|
||||
|
||||
static std::shared_ptr<MiLightRadioFactory> fromSettings(const Settings& settings);
|
||||
|
||||
};
|
||||
|
||||
class NRF24Factory : public MiLightRadioFactory {
|
||||
public:
|
||||
|
||||
NRF24Factory(
|
||||
uint8_t cePin,
|
||||
uint8_t csnPin,
|
||||
RF24PowerLevel rF24PowerLevel,
|
||||
const std::vector<RF24Channel>& channels,
|
||||
RF24Channel listenChannel
|
||||
);
|
||||
|
||||
virtual std::shared_ptr<MiLightRadio> create(const MiLightRadioConfig& config);
|
||||
|
||||
protected:
|
||||
|
||||
RF24 rf24;
|
||||
const std::vector<RF24Channel>& channels;
|
||||
const RF24Channel listenChannel;
|
||||
|
||||
};
|
||||
|
||||
class LT8900Factory : public MiLightRadioFactory {
|
||||
public:
|
||||
|
||||
LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag);
|
||||
|
||||
virtual std::shared_ptr<MiLightRadio> create(const MiLightRadioConfig& config);
|
||||
|
||||
protected:
|
||||
|
||||
uint8_t _csPin;
|
||||
uint8_t _resetPin;
|
||||
uint8_t _pktFlag;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
139
lib/Radio/NRF24MiLightRadio.cpp
Normal file
139
lib/Radio/NRF24MiLightRadio.cpp
Normal file
|
@ -0,0 +1,139 @@
|
|||
// Adapated from code from henryk
|
||||
|
||||
#include <PL1167_nRF24.h>
|
||||
#include <NRF24MiLightRadio.h>
|
||||
|
||||
#define PACKET_ID(packet, packet_length) ( (packet[1] << 8) | packet[packet_length - 1] )
|
||||
|
||||
NRF24MiLightRadio::NRF24MiLightRadio(
|
||||
RF24& rf24,
|
||||
const MiLightRadioConfig& config,
|
||||
const std::vector<RF24Channel>& channels,
|
||||
RF24Channel listenChannel
|
||||
)
|
||||
: channels(channels),
|
||||
listenChannelIx(static_cast<size_t>(listenChannel)),
|
||||
_pl1167(PL1167_nRF24(rf24)),
|
||||
_config(config),
|
||||
_waiting(false)
|
||||
{ }
|
||||
|
||||
int NRF24MiLightRadio::begin() {
|
||||
int retval = _pl1167.open();
|
||||
if (retval < 0) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
retval = configure();
|
||||
if (retval < 0) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
available();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int NRF24MiLightRadio::configure() {
|
||||
int retval = _pl1167.setSyncword(_config.syncwordBytes, MiLightRadioConfig::SYNCWORD_LENGTH);
|
||||
if (retval < 0) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
// +1 to be able to buffer the length
|
||||
retval = _pl1167.setMaxPacketLength(_config.packetLength + 1);
|
||||
if (retval < 0) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool NRF24MiLightRadio::available() {
|
||||
if (_waiting) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
printf("_waiting\n");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_pl1167.receive(_config.channels[listenChannelIx]) > 0) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
printf("NRF24MiLightRadio - received packet!\n");
|
||||
#endif
|
||||
size_t packet_length = sizeof(_packet);
|
||||
if (_pl1167.readFIFO(_packet, packet_length) < 0) {
|
||||
return false;
|
||||
}
|
||||
#ifdef DEBUG_PRINTF
|
||||
printf("NRF24MiLightRadio - Checking packet length (expecting %d, is %d)\n", _packet[0] + 1U, packet_length);
|
||||
#endif
|
||||
if (packet_length == 0 || packet_length != _packet[0] + 1U) {
|
||||
return false;
|
||||
}
|
||||
uint32_t packet_id = PACKET_ID(_packet, packet_length);
|
||||
#ifdef DEBUG_PRINTF
|
||||
printf("Packet id: %d\n", packet_id);
|
||||
#endif
|
||||
if (packet_id == _prev_packet_id) {
|
||||
_dupes_received++;
|
||||
} else {
|
||||
_prev_packet_id = packet_id;
|
||||
_waiting = true;
|
||||
}
|
||||
}
|
||||
|
||||
return _waiting;
|
||||
}
|
||||
|
||||
int NRF24MiLightRadio::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 NRF24MiLightRadio::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 NRF24MiLightRadio::resend() {
|
||||
for (std::vector<RF24Channel>::const_iterator it = channels.begin(); it != channels.end(); ++it) {
|
||||
size_t channelIx = static_cast<uint8_t>(*it);
|
||||
uint8_t channel = _config.channels[channelIx];
|
||||
|
||||
_pl1167.writeFIFO(_out_packet, _out_packet[0] + 1);
|
||||
_pl1167.transmit(channel);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const MiLightRadioConfig& NRF24MiLightRadio::config() {
|
||||
return _config;
|
||||
}
|
53
lib/Radio/NRF24MiLightRadio.h
Normal file
53
lib/Radio/NRF24MiLightRadio.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
#ifdef ARDUINO
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#endif
|
||||
|
||||
#include <RF24.h>
|
||||
#include <PL1167_nRF24.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
#include <MiLightRadio.h>
|
||||
#include <RF24Channel.h>
|
||||
#include <vector>
|
||||
|
||||
#ifndef _NRF24_MILIGHT_RADIO_H_
|
||||
#define _NRF24_MILIGHT_RADIO_H_
|
||||
|
||||
class NRF24MiLightRadio : public MiLightRadio {
|
||||
public:
|
||||
NRF24MiLightRadio(
|
||||
RF24& rf,
|
||||
const MiLightRadioConfig& config,
|
||||
const std::vector<RF24Channel>& channels,
|
||||
RF24Channel listenChannel
|
||||
);
|
||||
|
||||
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();
|
||||
int configure();
|
||||
const MiLightRadioConfig& config();
|
||||
|
||||
private:
|
||||
const std::vector<RF24Channel>& channels;
|
||||
const size_t listenChannelIx;
|
||||
|
||||
PL1167_nRF24 _pl1167;
|
||||
const MiLightRadioConfig& _config;
|
||||
uint32_t _prev_packet_id;
|
||||
|
||||
uint8_t _packet[10];
|
||||
uint8_t _out_packet[10];
|
||||
bool _waiting;
|
||||
int _dupes_received;
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif
|
261
lib/Radio/PL1167_nRF24.cpp
Normal file
261
lib/Radio/PL1167_nRF24.cpp
Normal file
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* PL1167_nRF24.cpp
|
||||
*
|
||||
* Adapted from work by henryk:
|
||||
* https://github.com/henryk/openmili
|
||||
* Created on: 29 May 2015
|
||||
* Author: henryk
|
||||
* Optimizations by khamann:
|
||||
* https://github.com/khmann/esp8266_milight_hub/blob/e3600cef75b102ff3be51a7afdb55ab7460fe712/lib/MiLight/PL1167_nRF24.cpp
|
||||
*
|
||||
*/
|
||||
|
||||
#include "PL1167_nRF24.h"
|
||||
#include <RadioUtils.h>
|
||||
#include <MiLightRadioConfig.h>
|
||||
|
||||
static uint16_t calc_crc(uint8_t *data, size_t data_length);
|
||||
|
||||
PL1167_nRF24::PL1167_nRF24(RF24 &radio)
|
||||
: _radio(radio)
|
||||
{ }
|
||||
|
||||
int PL1167_nRF24::open() {
|
||||
_radio.begin();
|
||||
_radio.setAutoAck(false);
|
||||
_radio.setDataRate(RF24_1MBPS);
|
||||
_radio.disableCRC();
|
||||
|
||||
_syncwordLength = MiLightRadioConfig::SYNCWORD_LENGTH;
|
||||
_radio.setAddressWidth(_syncwordLength);
|
||||
|
||||
return recalc_parameters();
|
||||
}
|
||||
|
||||
int PL1167_nRF24::recalc_parameters() {
|
||||
size_t nrf_address_length = _syncwordLength;
|
||||
|
||||
// +2 for CRC
|
||||
size_t packet_length = _maxPacketLength + 2;
|
||||
|
||||
// Read an extra byte if we don't include the trailer in the syncword
|
||||
if (_syncwordLength < 5) {
|
||||
++packet_length;
|
||||
}
|
||||
|
||||
if (packet_length > sizeof(_packet) || nrf_address_length < 3) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (_syncwordBytes != nullptr) {
|
||||
_radio.openWritingPipe(_syncwordBytes);
|
||||
_radio.openReadingPipe(1, _syncwordBytes);
|
||||
}
|
||||
|
||||
_receive_length = packet_length;
|
||||
|
||||
_radio.setChannel(2 + _channel);
|
||||
_radio.setPayloadSize( packet_length );
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PL1167_nRF24::setSyncword(const uint8_t syncword[], size_t syncwordLength) {
|
||||
_syncwordLength = syncwordLength;
|
||||
_syncwordBytes = syncword;
|
||||
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()) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
printf("Radio is available\n");
|
||||
#endif
|
||||
internal_receive();
|
||||
}
|
||||
|
||||
if(_received) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
if (_packet_length > 0) {
|
||||
printf("Received packet (len = %d)!\n", _packet_length);
|
||||
}
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
yield();
|
||||
}
|
||||
|
||||
_radio.stopListening();
|
||||
uint8_t tmp[sizeof(_packet)];
|
||||
int outp=0;
|
||||
|
||||
uint16_t crc = calc_crc(_packet, _packet_length);
|
||||
|
||||
// +1 for packet length
|
||||
// +2 for crc
|
||||
// = 3
|
||||
for (int inp = 0; inp < _packet_length + 3; inp++) {
|
||||
if (inp < _packet_length) {
|
||||
tmp[outp++] = reverseBits(_packet[inp]);}
|
||||
else if (inp < _packet_length + 2) {
|
||||
tmp[outp++] = reverseBits((crc >> ( (inp - _packet_length) * 8)) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
yield();
|
||||
|
||||
_radio.write(tmp, outp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The over-the-air packet structure sent by the PL1167 is as follows (lengths
|
||||
* measured in bits)
|
||||
*
|
||||
* Preamble ( 8) | Syncword (32) | Trailer ( 4) | Packet Len ( 8) | Packet (...)
|
||||
*
|
||||
* Note that because the Trailer is 4 bits, the remaining data is not byte-aligned.
|
||||
*
|
||||
* Bit-order is reversed.
|
||||
*
|
||||
*/
|
||||
int PL1167_nRF24::internal_receive() {
|
||||
uint8_t tmp[sizeof(_packet)];
|
||||
int outp = 0;
|
||||
|
||||
_radio.read(tmp, _receive_length);
|
||||
|
||||
// HACK HACK HACK: Reset radio
|
||||
open();
|
||||
|
||||
// Currently, the syncword width is set to 5 in order to include the
|
||||
// PL1167 trailer. The trailer is 4 bits, which pushes packet data
|
||||
// out of byte-alignment.
|
||||
//
|
||||
// The following code reads un-byte-aligned packet data.
|
||||
//
|
||||
// #ifdef DEBUG_PRINTF
|
||||
// Serial.printf_P(PSTR("Packet received (%d bytes) RAW: "), outp);
|
||||
// for (int i = 0; i < _receive_length; i++) {
|
||||
// Serial.printf_P(PSTR("%02X "), tmp[i]);
|
||||
// }
|
||||
// Serial.print(F("\n"));
|
||||
// #endif
|
||||
//
|
||||
// uint16_t buffer = tmp[0];
|
||||
//
|
||||
// for (int inp = 1; inp < _receive_length; inp++) {
|
||||
// uint8_t currentByte = tmp[inp];
|
||||
// tmp[outp++] = reverseBits((buffer << 4) | (currentByte >> 4));
|
||||
// buffer = (buffer << 8) | currentByte;
|
||||
// }
|
||||
|
||||
for (int inp = 0; inp < _receive_length; inp++) {
|
||||
tmp[outp++] = reverseBits(tmp[inp]);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.printf_P(PSTR("Packet received (%d bytes): "), outp);
|
||||
for (int i = 0; i < outp; i++) {
|
||||
Serial.printf_P(PSTR("%02X "), tmp[i]);
|
||||
}
|
||||
Serial.print(F("\n"));
|
||||
#endif
|
||||
|
||||
if (outp < 2) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.println(F("Failed CRC: outp < 2"));
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t crc = calc_crc(tmp, outp - 2);
|
||||
uint16_t recvCrc = (tmp[outp - 1] << 8) | tmp[outp - 2];
|
||||
|
||||
if ( crc != recvCrc ) {
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.printf_P(PSTR("Failed CRC: expected %04X, got %04X\n"), crc, recvCrc);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
outp -= 2;
|
||||
|
||||
memcpy(_packet, tmp, outp);
|
||||
|
||||
_packet_length = outp;
|
||||
_received = true;
|
||||
|
||||
#ifdef DEBUG_PRINTF
|
||||
Serial.printf_P(PSTR("Successfully parsed packet of length %d\n"), _packet_length);
|
||||
#endif
|
||||
|
||||
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;
|
||||
}
|
56
lib/Radio/PL1167_nRF24.h
Normal file
56
lib/Radio/PL1167_nRF24.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* PL1167_nRF24.h
|
||||
*
|
||||
* Created on: 29 May 2015
|
||||
* Author: henryk
|
||||
*/
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include "Arduino.h"
|
||||
#endif
|
||||
|
||||
#include "RF24.h"
|
||||
|
||||
// #define DEBUG_PRINTF
|
||||
|
||||
#ifndef PL1167_NRF24_H_
|
||||
#define PL1167_NRF24_H_
|
||||
|
||||
class PL1167_nRF24 {
|
||||
public:
|
||||
PL1167_nRF24(RF24& radio);
|
||||
int open();
|
||||
|
||||
int setSyncword(const uint8_t syncword[], size_t syncwordLength);
|
||||
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;
|
||||
|
||||
const uint8_t* _syncwordBytes = nullptr;
|
||||
uint8_t _syncwordLength = 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
lib/Radio/RadioUtils.cpp
Normal file
18
lib/Radio/RadioUtils.cpp
Normal file
|
@ -0,0 +1,18 @@
|
|||
#include <RadioUtils.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
uint8_t reverseBits(uint8_t byte) {
|
||||
uint8_t result = byte;
|
||||
uint8_t i = 7;
|
||||
|
||||
for (byte >>= 1; byte; byte >>= 1) {
|
||||
result <<= 1;
|
||||
result |= byte & 1;
|
||||
--i;
|
||||
}
|
||||
|
||||
return result << i;
|
||||
}
|
8
lib/Radio/RadioUtils.h
Normal file
8
lib/Radio/RadioUtils.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Reverse the bits of a given byte
|
||||
*/
|
||||
uint8_t reverseBits(uint8_t byte);
|
441
lib/SSDP/New_ESP8266SSDP.cpp
Normal file
441
lib/SSDP/New_ESP8266SSDP.cpp
Normal file
|
@ -0,0 +1,441 @@
|
|||
/*
|
||||
ESP8266 Simple Service Discovery
|
||||
Copyright (c) 2015 Hristo Gochkov
|
||||
|
||||
Original (Arduino) version by Filippo Sallemi, July 23, 2014.
|
||||
Can be found at: https://github.com/nomadnt/uSSDP
|
||||
|
||||
License (MIT license):
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
#define LWIP_OPEN_SRC
|
||||
#include <functional>
|
||||
#include "New_ESP8266SSDP.h"
|
||||
#include "WiFiUdp.h"
|
||||
#include "debug.h"
|
||||
|
||||
extern "C" {
|
||||
#include "osapi.h"
|
||||
#include "ets_sys.h"
|
||||
#include "user_interface.h"
|
||||
}
|
||||
|
||||
#include "lwip/opt.h"
|
||||
#include "lwip/udp.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/igmp.h"
|
||||
#include "lwip/mem.h"
|
||||
#include "include/UdpContext.h"
|
||||
|
||||
// #define DEBUG_SSDP Serial
|
||||
|
||||
#define SSDP_INTERVAL 1200
|
||||
#define SSDP_PORT 1900
|
||||
#define SSDP_METHOD_SIZE 10
|
||||
#define SSDP_URI_SIZE 2
|
||||
#define SSDP_BUFFER_SIZE 64
|
||||
#define SSDP_MULTICAST_TTL 2
|
||||
static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250);
|
||||
|
||||
|
||||
|
||||
static const char _ssdp_response_template[] PROGMEM =
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"EXT:\r\n";
|
||||
|
||||
static const char _ssdp_notify_template[] PROGMEM =
|
||||
"NOTIFY * HTTP/1.1\r\n"
|
||||
"HOST: 239.255.255.250:1900\r\n"
|
||||
"NTS: ssdp:alive\r\n";
|
||||
|
||||
static const char _ssdp_packet_template[] PROGMEM =
|
||||
"%s" // _ssdp_response_template / _ssdp_notify_template
|
||||
"CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL
|
||||
"SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber
|
||||
"USN: uuid:%s\r\n" // _uuid
|
||||
"%s: %s\r\n" // "NT" or "ST", _deviceType
|
||||
"LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
|
||||
"\r\n";
|
||||
|
||||
static const char _ssdp_schema_template[] PROGMEM =
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/xml\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"\r\n"
|
||||
"<?xml version=\"1.0\"?>"
|
||||
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
|
||||
"<specVersion>"
|
||||
"<major>1</major>"
|
||||
"<minor>0</minor>"
|
||||
"</specVersion>"
|
||||
"<URLBase>http://%u.%u.%u.%u:%u/</URLBase>" // WiFi.localIP(), _port
|
||||
"<device>"
|
||||
"<deviceType>%s</deviceType>"
|
||||
"<friendlyName>%s</friendlyName>"
|
||||
"<presentationURL>%s</presentationURL>"
|
||||
"<serialNumber>%s</serialNumber>"
|
||||
"<modelName>%s</modelName>"
|
||||
"<modelNumber>%s</modelNumber>"
|
||||
"<modelURL>%s</modelURL>"
|
||||
"<manufacturer>%s</manufacturer>"
|
||||
"<manufacturerURL>%s</manufacturerURL>"
|
||||
"<UDN>uuid:%s</UDN>"
|
||||
"</device>"
|
||||
// "<iconList>"
|
||||
// "<icon>"
|
||||
// "<mimetype>image/png</mimetype>"
|
||||
// "<height>48</height>"
|
||||
// "<width>48</width>"
|
||||
// "<depth>24</depth>"
|
||||
// "<url>icon48.png</url>"
|
||||
// "</icon>"
|
||||
// "<icon>"
|
||||
// "<mimetype>image/png</mimetype>"
|
||||
// "<height>120</height>"
|
||||
// "<width>120</width>"
|
||||
// "<depth>24</depth>"
|
||||
// "<url>icon120.png</url>"
|
||||
// "</icon>"
|
||||
// "</iconList>"
|
||||
"</root>\r\n"
|
||||
"\r\n";
|
||||
|
||||
|
||||
struct SSDPTimer {
|
||||
ETSTimer timer;
|
||||
};
|
||||
|
||||
SSDPClass::SSDPClass() :
|
||||
_server(0),
|
||||
_timer(new SSDPTimer),
|
||||
_port(80),
|
||||
_ttl(SSDP_MULTICAST_TTL),
|
||||
_respondToPort(0),
|
||||
_pending(false),
|
||||
_delay(0),
|
||||
_process_time(0),
|
||||
_notify_time(0)
|
||||
{
|
||||
_uuid[0] = '\0';
|
||||
_modelNumber[0] = '\0';
|
||||
sprintf(_deviceType, "urn:schemas-upnp-org:device:Basic:1");
|
||||
_friendlyName[0] = '\0';
|
||||
_presentationURL[0] = '\0';
|
||||
_serialNumber[0] = '\0';
|
||||
_modelName[0] = '\0';
|
||||
_modelURL[0] = '\0';
|
||||
_manufacturer[0] = '\0';
|
||||
_manufacturerURL[0] = '\0';
|
||||
sprintf(_schemaURL, "ssdp/schema.xml");
|
||||
}
|
||||
|
||||
SSDPClass::~SSDPClass(){
|
||||
delete _timer;
|
||||
}
|
||||
|
||||
bool SSDPClass::begin(){
|
||||
_pending = false;
|
||||
|
||||
uint32_t chipId = ESP.getChipId();
|
||||
sprintf(_uuid, "38323636-4558-4dda-9188-cda0e6%02x%02x%02x",
|
||||
(uint16_t) ((chipId >> 16) & 0xff),
|
||||
(uint16_t) ((chipId >> 8) & 0xff),
|
||||
(uint16_t) chipId & 0xff );
|
||||
|
||||
#ifdef DEBUG_SSDP
|
||||
DEBUG_SSDP.printf("SSDP UUID: %s\n", (char *)_uuid);
|
||||
#endif
|
||||
|
||||
if (_server) {
|
||||
_server->unref();
|
||||
_server = 0;
|
||||
}
|
||||
|
||||
_server = new UdpContext;
|
||||
_server->ref();
|
||||
|
||||
ip_addr_t ifaddr;
|
||||
ifaddr.addr = WiFi.localIP();
|
||||
ip_addr_t multicast_addr;
|
||||
multicast_addr.addr = (uint32_t) SSDP_MULTICAST_ADDR;
|
||||
if (igmp_joingroup(&ifaddr, &multicast_addr) != ERR_OK ) {
|
||||
DEBUGV("SSDP failed to join igmp group");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_server->listen(*IP_ADDR_ANY, SSDP_PORT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_server->setMulticastInterface(ifaddr);
|
||||
_server->setMulticastTTL(_ttl);
|
||||
_server->onRx(std::bind(&SSDPClass::_update, this));
|
||||
if (!_server->connect(multicast_addr, SSDP_PORT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_startTimer();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SSDPClass::_send(ssdp_method_t method){
|
||||
char buffer[1460];
|
||||
uint32_t ip = WiFi.localIP();
|
||||
|
||||
char valueBuffer[strlen(_ssdp_notify_template)+1];
|
||||
strcpy_P(valueBuffer, (method == NONE)?_ssdp_response_template:_ssdp_notify_template);
|
||||
|
||||
int len = snprintf_P(buffer, sizeof(buffer),
|
||||
_ssdp_packet_template,
|
||||
valueBuffer,
|
||||
SSDP_INTERVAL,
|
||||
_modelName, _modelNumber,
|
||||
_uuid,
|
||||
(method == NONE)?"ST":"NT",
|
||||
_deviceType,
|
||||
IP2STR(&ip), _port, _schemaURL
|
||||
);
|
||||
|
||||
_server->append(buffer, len);
|
||||
|
||||
ip_addr_t remoteAddr;
|
||||
uint16_t remotePort;
|
||||
if(method == NONE) {
|
||||
remoteAddr.addr = _respondToAddr;
|
||||
remotePort = _respondToPort;
|
||||
#ifdef DEBUG_SSDP
|
||||
DEBUG_SSDP.print("Sending Response to ");
|
||||
#endif
|
||||
} else {
|
||||
remoteAddr.addr = SSDP_MULTICAST_ADDR;
|
||||
remotePort = SSDP_PORT;
|
||||
#ifdef DEBUG_SSDP
|
||||
DEBUG_SSDP.println("Sending Notify to ");
|
||||
#endif
|
||||
}
|
||||
#ifdef DEBUG_SSDP
|
||||
DEBUG_SSDP.print(IPAddress(remoteAddr.addr));
|
||||
DEBUG_SSDP.print(":");
|
||||
DEBUG_SSDP.println(remotePort);
|
||||
#endif
|
||||
|
||||
_server->send(&remoteAddr, remotePort);
|
||||
}
|
||||
|
||||
void SSDPClass::schema(WiFiClient client){
|
||||
uint32_t ip = WiFi.localIP();
|
||||
char buffer[strlen(_ssdp_schema_template)+1];
|
||||
strcpy_P(buffer, _ssdp_schema_template);
|
||||
client.printf(buffer,
|
||||
IP2STR(&ip), _port,
|
||||
_deviceType,
|
||||
_friendlyName,
|
||||
_presentationURL,
|
||||
_serialNumber,
|
||||
_modelName,
|
||||
_modelNumber,
|
||||
_modelURL,
|
||||
_manufacturer,
|
||||
_manufacturerURL,
|
||||
_uuid
|
||||
);
|
||||
}
|
||||
|
||||
void SSDPClass::_update(){
|
||||
if(!_pending && _server->next()) {
|
||||
ssdp_method_t method = NONE;
|
||||
|
||||
_respondToAddr = _server->getRemoteAddress();
|
||||
_respondToPort = _server->getRemotePort();
|
||||
|
||||
typedef enum {METHOD, URI, PROTO, KEY, VALUE, ABORT} states;
|
||||
states state = METHOD;
|
||||
|
||||
typedef enum {START, MAN, ST, MX} headers;
|
||||
headers header = START;
|
||||
|
||||
uint8_t cursor = 0;
|
||||
uint8_t cr = 0;
|
||||
|
||||
char buffer[SSDP_BUFFER_SIZE] = {0};
|
||||
|
||||
while(_server->getSize() > 0){
|
||||
char c = _server->read();
|
||||
|
||||
(c == '\r' || c == '\n') ? cr++ : cr = 0;
|
||||
|
||||
switch(state){
|
||||
case METHOD:
|
||||
if(c == ' '){
|
||||
if(strcmp(buffer, "M-SEARCH") == 0) method = SEARCH;
|
||||
else if(strcmp(buffer, "NOTIFY") == 0) method = NOTIFY;
|
||||
|
||||
if(method == NONE) state = ABORT;
|
||||
else state = URI;
|
||||
cursor = 0;
|
||||
|
||||
} else if(cursor < SSDP_METHOD_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
|
||||
break;
|
||||
case URI:
|
||||
if(c == ' '){
|
||||
if(strcmp(buffer, "*")) state = ABORT;
|
||||
else state = PROTO;
|
||||
cursor = 0;
|
||||
} else if(cursor < SSDP_URI_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
|
||||
break;
|
||||
case PROTO:
|
||||
if(cr == 2){ state = KEY; cursor = 0; }
|
||||
break;
|
||||
case KEY:
|
||||
if(cr == 4){ _pending = true; _process_time = millis(); }
|
||||
else if(c == ' '){ cursor = 0; state = VALUE; }
|
||||
else if(c != '\r' && c != '\n' && c != ':' && cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
|
||||
break;
|
||||
case VALUE:
|
||||
if(cr == 2){
|
||||
switch(header){
|
||||
case START:
|
||||
break;
|
||||
case MAN:
|
||||
#ifdef DEBUG_SSDP
|
||||
DEBUG_SSDP.printf("MAN: %s\n", (char *)buffer);
|
||||
#endif
|
||||
break;
|
||||
case ST:
|
||||
if(strcmp(buffer, "ssdp:all")){
|
||||
state = ABORT;
|
||||
#ifdef DEBUG_SSDP
|
||||
DEBUG_SSDP.printf("REJECT: %s\n", (char *)buffer);
|
||||
#endif
|
||||
}
|
||||
// if the search type matches our type, we should respond instead of ABORT
|
||||
if(strcmp(buffer, _deviceType) == 0){
|
||||
_pending = true;
|
||||
_process_time = millis();
|
||||
state = KEY;
|
||||
}
|
||||
break;
|
||||
case MX:
|
||||
_delay = random(0, atoi(buffer)) * 1000L;
|
||||
break;
|
||||
}
|
||||
|
||||
if(state != ABORT){ state = KEY; header = START; cursor = 0; }
|
||||
} else if(c != '\r' && c != '\n'){
|
||||
if(header == START){
|
||||
if(strncmp(buffer, "MA", 2) == 0) header = MAN;
|
||||
else if(strcmp(buffer, "ST") == 0) header = ST;
|
||||
else if(strcmp(buffer, "MX") == 0) header = MX;
|
||||
}
|
||||
|
||||
if(cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
|
||||
}
|
||||
break;
|
||||
case ABORT:
|
||||
_pending = false; _delay = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(_pending && (millis() - _process_time) > _delay){
|
||||
_pending = false; _delay = 0;
|
||||
_send(NONE);
|
||||
} else if(_notify_time == 0 || (millis() - _notify_time) > (SSDP_INTERVAL * 1000L)){
|
||||
_notify_time = millis();
|
||||
_send(NOTIFY);
|
||||
}
|
||||
|
||||
if (_pending) {
|
||||
while (_server->next())
|
||||
_server->flush();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SSDPClass::setSchemaURL(const char *url){
|
||||
strlcpy(_schemaURL, url, sizeof(_schemaURL));
|
||||
}
|
||||
|
||||
void SSDPClass::setHTTPPort(uint16_t port){
|
||||
_port = port;
|
||||
}
|
||||
|
||||
void SSDPClass::setDeviceType(const char *deviceType){
|
||||
strlcpy(_deviceType, deviceType, sizeof(_deviceType));
|
||||
}
|
||||
|
||||
void SSDPClass::setName(const char *name){
|
||||
strlcpy(_friendlyName, name, sizeof(_friendlyName));
|
||||
}
|
||||
|
||||
void SSDPClass::setURL(const char *url){
|
||||
strlcpy(_presentationURL, url, sizeof(_presentationURL));
|
||||
}
|
||||
|
||||
void SSDPClass::setSerialNumber(const char *serialNumber){
|
||||
strlcpy(_serialNumber, serialNumber, sizeof(_serialNumber));
|
||||
}
|
||||
|
||||
void SSDPClass::setSerialNumber(const uint32_t serialNumber){
|
||||
snprintf(_serialNumber, sizeof(uint32_t)*2+1, "%08X", serialNumber);
|
||||
}
|
||||
|
||||
void SSDPClass::setModelName(const char *name){
|
||||
strlcpy(_modelName, name, sizeof(_modelName));
|
||||
}
|
||||
|
||||
void SSDPClass::setModelNumber(const char *num){
|
||||
strlcpy(_modelNumber, num, sizeof(_modelNumber));
|
||||
}
|
||||
|
||||
void SSDPClass::setModelURL(const char *url){
|
||||
strlcpy(_modelURL, url, sizeof(_modelURL));
|
||||
}
|
||||
|
||||
void SSDPClass::setManufacturer(const char *name){
|
||||
strlcpy(_manufacturer, name, sizeof(_manufacturer));
|
||||
}
|
||||
|
||||
void SSDPClass::setManufacturerURL(const char *url){
|
||||
strlcpy(_manufacturerURL, url, sizeof(_manufacturerURL));
|
||||
}
|
||||
|
||||
void SSDPClass::setTTL(const uint8_t ttl){
|
||||
_ttl = ttl;
|
||||
}
|
||||
|
||||
void SSDPClass::_onTimerStatic(SSDPClass* self) {
|
||||
self->_update();
|
||||
}
|
||||
|
||||
void SSDPClass::_startTimer() {
|
||||
ETSTimer* tm = &(_timer->timer);
|
||||
const int interval = 1000;
|
||||
os_timer_disarm(tm);
|
||||
os_timer_setfn(tm, reinterpret_cast<ETSTimerFunc*>(&SSDPClass::_onTimerStatic), reinterpret_cast<void*>(this));
|
||||
os_timer_arm(tm, interval, 1 /* repeat */);
|
||||
}
|
||||
|
||||
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP)
|
||||
SSDPClass SSDP;
|
||||
#endif
|
128
lib/SSDP/New_ESP8266SSDP.h
Normal file
128
lib/SSDP/New_ESP8266SSDP.h
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
ESP8266 Simple Service Discovery
|
||||
Copyright (c) 2015 Hristo Gochkov
|
||||
|
||||
Original (Arduino) version by Filippo Sallemi, July 23, 2014.
|
||||
Can be found at: https://github.com/nomadnt/uSSDP
|
||||
|
||||
License (MIT license):
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#ifndef ESP8266SSDP_H
|
||||
#define ESP8266SSDP_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
|
||||
class UdpContext;
|
||||
|
||||
#define SSDP_UUID_SIZE 37
|
||||
#define SSDP_SCHEMA_URL_SIZE 64
|
||||
#define SSDP_DEVICE_TYPE_SIZE 64
|
||||
#define SSDP_FRIENDLY_NAME_SIZE 64
|
||||
#define SSDP_SERIAL_NUMBER_SIZE 32
|
||||
#define SSDP_PRESENTATION_URL_SIZE 128
|
||||
#define SSDP_MODEL_NAME_SIZE 64
|
||||
#define SSDP_MODEL_URL_SIZE 128
|
||||
#define SSDP_MODEL_VERSION_SIZE 32
|
||||
#define SSDP_MANUFACTURER_SIZE 64
|
||||
#define SSDP_MANUFACTURER_URL_SIZE 128
|
||||
|
||||
typedef enum {
|
||||
NONE,
|
||||
SEARCH,
|
||||
NOTIFY
|
||||
} ssdp_method_t;
|
||||
|
||||
|
||||
struct SSDPTimer;
|
||||
|
||||
class SSDPClass{
|
||||
public:
|
||||
SSDPClass();
|
||||
~SSDPClass();
|
||||
|
||||
bool begin();
|
||||
|
||||
void schema(WiFiClient client);
|
||||
|
||||
void setDeviceType(const String& deviceType) { setDeviceType(deviceType.c_str()); }
|
||||
void setDeviceType(const char *deviceType);
|
||||
void setName(const String& name) { setName(name.c_str()); }
|
||||
void setName(const char *name);
|
||||
void setURL(const String& url) { setURL(url.c_str()); }
|
||||
void setURL(const char *url);
|
||||
void setSchemaURL(const String& url) { setSchemaURL(url.c_str()); }
|
||||
void setSchemaURL(const char *url);
|
||||
void setSerialNumber(const String& serialNumber) { setSerialNumber(serialNumber.c_str()); }
|
||||
void setSerialNumber(const char *serialNumber);
|
||||
void setSerialNumber(const uint32_t serialNumber);
|
||||
void setModelName(const String& name) { setModelName(name.c_str()); }
|
||||
void setModelName(const char *name);
|
||||
void setModelNumber(const String& num) { setModelNumber(num.c_str()); }
|
||||
void setModelNumber(const char *num);
|
||||
void setModelURL(const String& url) { setModelURL(url.c_str()); }
|
||||
void setModelURL(const char *url);
|
||||
void setManufacturer(const String& name) { setManufacturer(name.c_str()); }
|
||||
void setManufacturer(const char *name);
|
||||
void setManufacturerURL(const String& url) { setManufacturerURL(url.c_str()); }
|
||||
void setManufacturerURL(const char *url);
|
||||
void setHTTPPort(uint16_t port);
|
||||
void setTTL(uint8_t ttl);
|
||||
|
||||
protected:
|
||||
void _send(ssdp_method_t method);
|
||||
void _update();
|
||||
void _startTimer();
|
||||
static void _onTimerStatic(SSDPClass* self);
|
||||
|
||||
UdpContext* _server;
|
||||
SSDPTimer* _timer;
|
||||
uint16_t _port;
|
||||
uint8_t _ttl;
|
||||
|
||||
IPAddress _respondToAddr;
|
||||
uint16_t _respondToPort;
|
||||
|
||||
bool _pending;
|
||||
unsigned short _delay;
|
||||
unsigned long _process_time;
|
||||
unsigned long _notify_time;
|
||||
|
||||
char _schemaURL[SSDP_SCHEMA_URL_SIZE];
|
||||
char _uuid[SSDP_UUID_SIZE];
|
||||
char _deviceType[SSDP_DEVICE_TYPE_SIZE];
|
||||
char _friendlyName[SSDP_FRIENDLY_NAME_SIZE];
|
||||
char _serialNumber[SSDP_SERIAL_NUMBER_SIZE];
|
||||
char _presentationURL[SSDP_PRESENTATION_URL_SIZE];
|
||||
char _manufacturer[SSDP_MANUFACTURER_SIZE];
|
||||
char _manufacturerURL[SSDP_MANUFACTURER_URL_SIZE];
|
||||
char _modelName[SSDP_MODEL_NAME_SIZE];
|
||||
char _modelURL[SSDP_MODEL_URL_SIZE];
|
||||
char _modelNumber[SSDP_MODEL_VERSION_SIZE];
|
||||
};
|
||||
|
||||
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP)
|
||||
extern SSDPClass SSDP;
|
||||
#endif
|
||||
|
||||
#endif
|
28
lib/Settings/AboutHelper.cpp
Normal file
28
lib/Settings/AboutHelper.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#include <AboutHelper.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Settings.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
|
||||
String AboutHelper::generateAboutString(bool abbreviated) {
|
||||
DynamicJsonDocument buffer(1024);
|
||||
|
||||
generateAboutObject(buffer, abbreviated);
|
||||
|
||||
String body;
|
||||
serializeJson(buffer, body);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
void AboutHelper::generateAboutObject(JsonDocument& obj, bool abbreviated) {
|
||||
obj["firmware"] = QUOTE(FIRMWARE_NAME);
|
||||
obj["version"] = QUOTE(MILIGHT_HUB_VERSION);
|
||||
obj["ip_address"] = WiFi.localIP().toString();
|
||||
obj["reset_reason"] = ESP.getResetReason();
|
||||
|
||||
if (! abbreviated) {
|
||||
obj["variant"] = QUOTE(FIRMWARE_VARIANT);
|
||||
obj["free_heap"] = ESP.getFreeHeap();
|
||||
obj["arduino_version"] = ESP.getCoreVersion();
|
||||
}
|
||||
}
|
13
lib/Settings/AboutHelper.h
Normal file
13
lib/Settings/AboutHelper.h
Normal file
|
@ -0,0 +1,13 @@
|
|||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#ifndef _ABOUT_STRING_HELPER_H
|
||||
#define _ABOUT_STRING_HELPER_H
|
||||
|
||||
class AboutHelper {
|
||||
public:
|
||||
static String generateAboutString(bool abbreviated = false);
|
||||
static void generateAboutObject(JsonDocument& obj, bool abbreviated = false);
|
||||
};
|
||||
|
||||
#endif
|
383
lib/Settings/Settings.cpp
Normal file
383
lib/Settings/Settings.cpp
Normal file
|
@ -0,0 +1,383 @@
|
|||
#include <Settings.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <FS.h>
|
||||
#include <IntParsing.h>
|
||||
#include <algorithm>
|
||||
#include <JsonHelpers.h>
|
||||
|
||||
#define PORT_POSITION(s) ( s.indexOf(':') )
|
||||
|
||||
GatewayConfig::GatewayConfig(uint16_t deviceId, uint16_t port, uint8_t protocolVersion)
|
||||
: deviceId(deviceId)
|
||||
, port(port)
|
||||
, protocolVersion(protocolVersion)
|
||||
{ }
|
||||
|
||||
bool Settings::isAuthenticationEnabled() const {
|
||||
return adminUsername.length() > 0 && adminPassword.length() > 0;
|
||||
}
|
||||
|
||||
const String& Settings::getUsername() const {
|
||||
return adminUsername;
|
||||
}
|
||||
|
||||
const String& Settings::getPassword() const {
|
||||
return adminPassword;
|
||||
}
|
||||
|
||||
bool Settings::isAutoRestartEnabled() {
|
||||
return _autoRestartPeriod > 0;
|
||||
}
|
||||
|
||||
size_t Settings::getAutoRestartPeriod() {
|
||||
if (_autoRestartPeriod == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return std::max(_autoRestartPeriod, static_cast<size_t>(MINIMUM_RESTART_PERIOD));
|
||||
}
|
||||
|
||||
void Settings::updateDeviceIds(JsonArray arr) {
|
||||
this->deviceIds.clear();
|
||||
|
||||
for (size_t i = 0; i < arr.size(); ++i) {
|
||||
this->deviceIds.push_back(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::updateGatewayConfigs(JsonArray arr) {
|
||||
gatewayConfigs.clear();
|
||||
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
JsonArray params = arr[i];
|
||||
|
||||
if (params.size() == 3) {
|
||||
std::shared_ptr<GatewayConfig> ptr = std::make_shared<GatewayConfig>(parseInt<uint16_t>(params[0]), params[1], params[2]);
|
||||
gatewayConfigs.push_back(std::move(ptr));
|
||||
} else {
|
||||
Serial.print(F("Settings - skipped parsing gateway ports settings for element #"));
|
||||
Serial.println(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::patch(JsonObject parsedSettings) {
|
||||
if (parsedSettings.isNull()) {
|
||||
Serial.println(F("Skipping patching loaded settings. Parsed settings was null."));
|
||||
return;
|
||||
}
|
||||
|
||||
this->setIfPresent(parsedSettings, "admin_username", adminUsername);
|
||||
this->setIfPresent(parsedSettings, "admin_password", adminPassword);
|
||||
this->setIfPresent(parsedSettings, "ce_pin", cePin);
|
||||
this->setIfPresent(parsedSettings, "csn_pin", csnPin);
|
||||
this->setIfPresent(parsedSettings, "reset_pin", resetPin);
|
||||
this->setIfPresent(parsedSettings, "led_pin", ledPin);
|
||||
this->setIfPresent(parsedSettings, "packet_repeats", packetRepeats);
|
||||
this->setIfPresent(parsedSettings, "http_repeat_factor", httpRepeatFactor);
|
||||
this->setIfPresent(parsedSettings, "auto_restart_period", _autoRestartPeriod);
|
||||
this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer);
|
||||
this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername);
|
||||
this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword);
|
||||
this->setIfPresent(parsedSettings, "mqtt_topic_pattern", mqttTopicPattern);
|
||||
this->setIfPresent(parsedSettings, "mqtt_update_topic_pattern", mqttUpdateTopicPattern);
|
||||
this->setIfPresent(parsedSettings, "mqtt_state_topic_pattern", mqttStateTopicPattern);
|
||||
this->setIfPresent(parsedSettings, "mqtt_client_status_topic", mqttClientStatusTopic);
|
||||
this->setIfPresent(parsedSettings, "simple_mqtt_client_status", simpleMqttClientStatus);
|
||||
this->setIfPresent(parsedSettings, "discovery_port", discoveryPort);
|
||||
this->setIfPresent(parsedSettings, "listen_repeats", listenRepeats);
|
||||
this->setIfPresent(parsedSettings, "state_flush_interval", stateFlushInterval);
|
||||
this->setIfPresent(parsedSettings, "mqtt_state_rate_limit", mqttStateRateLimit);
|
||||
this->setIfPresent(parsedSettings, "mqtt_debounce_delay", mqttDebounceDelay);
|
||||
this->setIfPresent(parsedSettings, "mqtt_retain", mqttRetain);
|
||||
this->setIfPresent(parsedSettings, "packet_repeat_throttle_threshold", packetRepeatThrottleThreshold);
|
||||
this->setIfPresent(parsedSettings, "packet_repeat_throttle_sensitivity", packetRepeatThrottleSensitivity);
|
||||
this->setIfPresent(parsedSettings, "packet_repeat_minimum", packetRepeatMinimum);
|
||||
this->setIfPresent(parsedSettings, "enable_automatic_mode_switching", enableAutomaticModeSwitching);
|
||||
this->setIfPresent(parsedSettings, "led_mode_packet_count", ledModePacketCount);
|
||||
this->setIfPresent(parsedSettings, "hostname", hostname);
|
||||
this->setIfPresent(parsedSettings, "wifi_static_ip", wifiStaticIP);
|
||||
this->setIfPresent(parsedSettings, "wifi_static_ip_gateway", wifiStaticIPGateway);
|
||||
this->setIfPresent(parsedSettings, "wifi_static_ip_netmask", wifiStaticIPNetmask);
|
||||
this->setIfPresent(parsedSettings, "packet_repeats_per_loop", packetRepeatsPerLoop);
|
||||
this->setIfPresent(parsedSettings, "home_assistant_discovery_prefix", homeAssistantDiscoveryPrefix);
|
||||
this->setIfPresent(parsedSettings, "default_transition_period", defaultTransitionPeriod);
|
||||
|
||||
if (parsedSettings.containsKey("wifi_mode")) {
|
||||
this->wifiMode = wifiModeFromString(parsedSettings["wifi_mode"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("rf24_channels")) {
|
||||
JsonArray arr = parsedSettings["rf24_channels"];
|
||||
rf24Channels = JsonHelpers::jsonArrToVector<RF24Channel, String>(arr, RF24ChannelHelpers::valueFromName);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("rf24_listen_channel")) {
|
||||
this->rf24ListenChannel = RF24ChannelHelpers::valueFromName(parsedSettings["rf24_listen_channel"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("rf24_power_level")) {
|
||||
this->rf24PowerLevel = RF24PowerLevelHelpers::valueFromName(parsedSettings["rf24_power_level"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("led_mode_wifi_config")) {
|
||||
this->ledModeWifiConfig = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_config"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("led_mode_wifi_failed")) {
|
||||
this->ledModeWifiFailed = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_failed"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("led_mode_operating")) {
|
||||
this->ledModeOperating = LEDStatus::stringToLEDMode(parsedSettings["led_mode_operating"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("led_mode_packet")) {
|
||||
this->ledModePacket = LEDStatus::stringToLEDMode(parsedSettings["led_mode_packet"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("radio_interface_type")) {
|
||||
this->radioInterfaceType = Settings::typeFromString(parsedSettings["radio_interface_type"]);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("device_ids")) {
|
||||
JsonArray arr = parsedSettings["device_ids"];
|
||||
updateDeviceIds(arr);
|
||||
}
|
||||
if (parsedSettings.containsKey("gateway_configs")) {
|
||||
JsonArray arr = parsedSettings["gateway_configs"];
|
||||
updateGatewayConfigs(arr);
|
||||
}
|
||||
if (parsedSettings.containsKey("group_state_fields")) {
|
||||
JsonArray arr = parsedSettings["group_state_fields"];
|
||||
groupStateFields = JsonHelpers::jsonArrToVector<GroupStateField, const char*>(arr, GroupStateFieldHelpers::getFieldByName);
|
||||
}
|
||||
|
||||
if (parsedSettings.containsKey("group_id_aliases")) {
|
||||
parseGroupIdAliases(parsedSettings);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<String, BulbId>::const_iterator Settings::findAlias(MiLightRemoteType deviceType, uint16_t deviceId, uint8_t groupId) {
|
||||
BulbId searchId{ deviceId, groupId, deviceType };
|
||||
|
||||
for (auto it = groupIdAliases.begin(); it != groupIdAliases.end(); ++it) {
|
||||
if (searchId == it->second) {
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
return groupIdAliases.end();
|
||||
}
|
||||
|
||||
void Settings::parseGroupIdAliases(JsonObject json) {
|
||||
JsonObject aliases = json["group_id_aliases"];
|
||||
|
||||
// Save group IDs that were deleted so that they can be processed by discovery
|
||||
// if necessary
|
||||
for (auto it = groupIdAliases.begin(); it != groupIdAliases.end(); ++it) {
|
||||
deletedGroupIdAliases[it->second.getCompactId()] = it->second;
|
||||
}
|
||||
|
||||
groupIdAliases.clear();
|
||||
|
||||
for (JsonPair kv : aliases) {
|
||||
JsonArray bulbIdProps = kv.value();
|
||||
BulbId bulbId = {
|
||||
bulbIdProps[1].as<uint16_t>(),
|
||||
bulbIdProps[2].as<uint8_t>(),
|
||||
MiLightRemoteTypeHelpers::remoteTypeFromString(bulbIdProps[0].as<String>())
|
||||
};
|
||||
groupIdAliases[kv.key().c_str()] = bulbId;
|
||||
|
||||
// If added this round, do not mark as deleted.
|
||||
deletedGroupIdAliases.erase(bulbId.getCompactId());
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::dumpGroupIdAliases(JsonObject json) {
|
||||
JsonObject aliases = json.createNestedObject("group_id_aliases");
|
||||
|
||||
for (std::map<String, BulbId>::iterator itr = groupIdAliases.begin(); itr != groupIdAliases.end(); ++itr) {
|
||||
JsonArray bulbProps = aliases.createNestedArray(itr->first);
|
||||
BulbId bulbId = itr->second;
|
||||
bulbProps.add(MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType));
|
||||
bulbProps.add(bulbId.deviceId);
|
||||
bulbProps.add(bulbId.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::load(Settings& settings) {
|
||||
if (SPIFFS.exists(SETTINGS_FILE)) {
|
||||
// Clear in-memory settings
|
||||
settings = Settings();
|
||||
|
||||
File f = SPIFFS.open(SETTINGS_FILE, "r");
|
||||
|
||||
DynamicJsonDocument json(MILIGHT_HUB_SETTINGS_BUFFER_SIZE);
|
||||
auto error = deserializeJson(json, f);
|
||||
f.close();
|
||||
|
||||
if (! error) {
|
||||
JsonObject parsedSettings = json.as<JsonObject>();
|
||||
settings.patch(parsedSettings);
|
||||
} else {
|
||||
Serial.print(F("Error parsing saved settings file: "));
|
||||
Serial.println(error.c_str());
|
||||
}
|
||||
} else {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
|
||||
String Settings::toJson(const bool prettyPrint) {
|
||||
String buffer = "";
|
||||
StringStream s(buffer);
|
||||
serialize(s, prettyPrint);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void Settings::save() {
|
||||
File f = SPIFFS.open(SETTINGS_FILE, "w");
|
||||
|
||||
if (!f) {
|
||||
Serial.println(F("Opening settings file failed"));
|
||||
} else {
|
||||
serialize(f);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::serialize(Print& stream, const bool prettyPrint) {
|
||||
DynamicJsonDocument root(MILIGHT_HUB_SETTINGS_BUFFER_SIZE);
|
||||
|
||||
root["admin_username"] = this->adminUsername;
|
||||
root["admin_password"] = this->adminPassword;
|
||||
root["ce_pin"] = this->cePin;
|
||||
root["csn_pin"] = this->csnPin;
|
||||
root["reset_pin"] = this->resetPin;
|
||||
root["led_pin"] = this->ledPin;
|
||||
root["radio_interface_type"] = typeToString(this->radioInterfaceType);
|
||||
root["packet_repeats"] = this->packetRepeats;
|
||||
root["http_repeat_factor"] = this->httpRepeatFactor;
|
||||
root["auto_restart_period"] = this->_autoRestartPeriod;
|
||||
root["mqtt_server"] = this->_mqttServer;
|
||||
root["mqtt_username"] = this->mqttUsername;
|
||||
root["mqtt_password"] = this->mqttPassword;
|
||||
root["mqtt_topic_pattern"] = this->mqttTopicPattern;
|
||||
root["mqtt_update_topic_pattern"] = this->mqttUpdateTopicPattern;
|
||||
root["mqtt_state_topic_pattern"] = this->mqttStateTopicPattern;
|
||||
root["mqtt_client_status_topic"] = this->mqttClientStatusTopic;
|
||||
root["simple_mqtt_client_status"] = this->simpleMqttClientStatus;
|
||||
root["discovery_port"] = this->discoveryPort;
|
||||
root["listen_repeats"] = this->listenRepeats;
|
||||
root["state_flush_interval"] = this->stateFlushInterval;
|
||||
root["mqtt_state_rate_limit"] = this->mqttStateRateLimit;
|
||||
root["mqtt_debounce_delay"] = this->mqttDebounceDelay;
|
||||
root["mqtt_retain"] = this->mqttRetain;
|
||||
root["packet_repeat_throttle_sensitivity"] = this->packetRepeatThrottleSensitivity;
|
||||
root["packet_repeat_throttle_threshold"] = this->packetRepeatThrottleThreshold;
|
||||
root["packet_repeat_minimum"] = this->packetRepeatMinimum;
|
||||
root["enable_automatic_mode_switching"] = this->enableAutomaticModeSwitching;
|
||||
root["led_mode_wifi_config"] = LEDStatus::LEDModeToString(this->ledModeWifiConfig);
|
||||
root["led_mode_wifi_failed"] = LEDStatus::LEDModeToString(this->ledModeWifiFailed);
|
||||
root["led_mode_operating"] = LEDStatus::LEDModeToString(this->ledModeOperating);
|
||||
root["led_mode_packet"] = LEDStatus::LEDModeToString(this->ledModePacket);
|
||||
root["led_mode_packet_count"] = this->ledModePacketCount;
|
||||
root["hostname"] = this->hostname;
|
||||
root["rf24_power_level"] = RF24PowerLevelHelpers::nameFromValue(this->rf24PowerLevel);
|
||||
root["rf24_listen_channel"] = RF24ChannelHelpers::nameFromValue(rf24ListenChannel);
|
||||
root["wifi_static_ip"] = this->wifiStaticIP;
|
||||
root["wifi_static_ip_gateway"] = this->wifiStaticIPGateway;
|
||||
root["wifi_static_ip_netmask"] = this->wifiStaticIPNetmask;
|
||||
root["packet_repeats_per_loop"] = this->packetRepeatsPerLoop;
|
||||
root["home_assistant_discovery_prefix"] = this->homeAssistantDiscoveryPrefix;
|
||||
root["wifi_mode"] = wifiModeToString(this->wifiMode);
|
||||
root["default_transition_period"] = this->defaultTransitionPeriod;
|
||||
|
||||
JsonArray channelArr = root.createNestedArray("rf24_channels");
|
||||
JsonHelpers::vectorToJsonArr<RF24Channel, String>(channelArr, rf24Channels, RF24ChannelHelpers::nameFromValue);
|
||||
|
||||
JsonArray deviceIdsArr = root.createNestedArray("device_ids");
|
||||
JsonHelpers::copyFrom<uint16_t>(deviceIdsArr, this->deviceIds);
|
||||
|
||||
JsonArray gatewayConfigsArr = root.createNestedArray("gateway_configs");
|
||||
for (size_t i = 0; i < this->gatewayConfigs.size(); i++) {
|
||||
JsonArray elmt = gatewayConfigsArr.createNestedArray();
|
||||
elmt.add(this->gatewayConfigs[i]->deviceId);
|
||||
elmt.add(this->gatewayConfigs[i]->port);
|
||||
elmt.add(this->gatewayConfigs[i]->protocolVersion);
|
||||
}
|
||||
|
||||
JsonArray groupStateFieldArr = root.createNestedArray("group_state_fields");
|
||||
JsonHelpers::vectorToJsonArr<GroupStateField, const char*>(groupStateFieldArr, groupStateFields, GroupStateFieldHelpers::getFieldName);
|
||||
|
||||
dumpGroupIdAliases(root.as<JsonObject>());
|
||||
|
||||
if (prettyPrint) {
|
||||
serializeJsonPretty(root, stream);
|
||||
} else {
|
||||
serializeJson(root, stream);
|
||||
}
|
||||
}
|
||||
|
||||
String Settings::mqttServer() {
|
||||
int pos = PORT_POSITION(_mqttServer);
|
||||
|
||||
if (pos == -1) {
|
||||
return _mqttServer;
|
||||
} else {
|
||||
return _mqttServer.substring(0, pos);
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t Settings::mqttPort() {
|
||||
int pos = PORT_POSITION(_mqttServer);
|
||||
|
||||
if (pos == -1) {
|
||||
return DEFAULT_MQTT_PORT;
|
||||
} else {
|
||||
return atoi(_mqttServer.c_str() + pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
RadioInterfaceType Settings::typeFromString(const String& s) {
|
||||
if (s.equalsIgnoreCase("lt8900")) {
|
||||
return LT8900;
|
||||
} else {
|
||||
return nRF24;
|
||||
}
|
||||
}
|
||||
|
||||
String Settings::typeToString(RadioInterfaceType type) {
|
||||
switch (type) {
|
||||
case LT8900:
|
||||
return "LT8900";
|
||||
|
||||
case nRF24:
|
||||
default:
|
||||
return "nRF24";
|
||||
}
|
||||
}
|
||||
|
||||
WifiMode Settings::wifiModeFromString(const String& mode) {
|
||||
if (mode.equalsIgnoreCase("b")) {
|
||||
return WifiMode::B;
|
||||
} else if (mode.equalsIgnoreCase("g")) {
|
||||
return WifiMode::G;
|
||||
} else {
|
||||
return WifiMode::N;
|
||||
}
|
||||
}
|
||||
|
||||
String Settings::wifiModeToString(WifiMode mode) {
|
||||
switch (mode) {
|
||||
case WifiMode::B:
|
||||
return "b";
|
||||
case WifiMode::G:
|
||||
return "g";
|
||||
case WifiMode::N:
|
||||
default:
|
||||
return "n";
|
||||
}
|
||||
}
|
219
lib/Settings/Settings.h
Normal file
219
lib/Settings/Settings.h
Normal file
|
@ -0,0 +1,219 @@
|
|||
#include <Arduino.h>
|
||||
#include <StringStream.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <GroupStateField.h>
|
||||
#include <RF24PowerLevel.h>
|
||||
#include <RF24Channel.h>
|
||||
#include <Size.h>
|
||||
#include <LEDStatus.h>
|
||||
#include <AuthProviders.h>
|
||||
|
||||
#include <MiLightRemoteType.h>
|
||||
#include <BulbId.h>
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
|
||||
#ifndef _SETTINGS_H_INCLUDED
|
||||
#define _SETTINGS_H_INCLUDED
|
||||
|
||||
#ifndef MILIGHT_HUB_SETTINGS_BUFFER_SIZE
|
||||
#define MILIGHT_HUB_SETTINGS_BUFFER_SIZE 4096
|
||||
#endif
|
||||
|
||||
#define XQUOTE(x) #x
|
||||
#define QUOTE(x) XQUOTE(x)
|
||||
|
||||
#ifndef FIRMWARE_NAME
|
||||
#define FIRMWARE_NAME unknown
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VARIANT
|
||||
#define FIRMWARE_VARIANT unknown
|
||||
#endif
|
||||
|
||||
#ifndef MILIGHT_HUB_VERSION
|
||||
#define MILIGHT_HUB_VERSION unknown
|
||||
#endif
|
||||
|
||||
#ifndef MILIGHT_MAX_STATE_ITEMS
|
||||
#define MILIGHT_MAX_STATE_ITEMS 100
|
||||
#endif
|
||||
|
||||
#ifndef MILIGHT_MAX_STALE_MQTT_GROUPS
|
||||
#define MILIGHT_MAX_STALE_MQTT_GROUPS 10
|
||||
#endif
|
||||
|
||||
#define SETTINGS_FILE "/config.json"
|
||||
#define SETTINGS_TERMINATOR '\0'
|
||||
|
||||
#define WEB_INDEX_FILENAME "/web/index.html"
|
||||
|
||||
#define MILIGHT_GITHUB_USER "sidoh"
|
||||
#define MILIGHT_GITHUB_REPO "esp8266_milight_hub"
|
||||
#define MILIGHT_REPO_WEB_PATH "/data/web/index.html"
|
||||
|
||||
#define MINIMUM_RESTART_PERIOD 1
|
||||
#define DEFAULT_MQTT_PORT 1883
|
||||
#define MAX_IP_ADDR_LEN 15
|
||||
|
||||
enum RadioInterfaceType {
|
||||
nRF24 = 0,
|
||||
LT8900 = 1,
|
||||
};
|
||||
|
||||
enum class WifiMode {
|
||||
B, G, N
|
||||
};
|
||||
|
||||
static const std::vector<GroupStateField> DEFAULT_GROUP_STATE_FIELDS({
|
||||
GroupStateField::STATE,
|
||||
GroupStateField::BRIGHTNESS,
|
||||
GroupStateField::COMPUTED_COLOR,
|
||||
GroupStateField::MODE,
|
||||
GroupStateField::COLOR_TEMP,
|
||||
GroupStateField::BULB_MODE
|
||||
});
|
||||
|
||||
struct GatewayConfig {
|
||||
GatewayConfig(uint16_t deviceId, uint16_t port, uint8_t protocolVersion);
|
||||
|
||||
const uint16_t deviceId;
|
||||
const uint16_t port;
|
||||
const uint8_t protocolVersion;
|
||||
};
|
||||
|
||||
class Settings {
|
||||
public:
|
||||
Settings() :
|
||||
adminUsername(""),
|
||||
adminPassword(""),
|
||||
// CE and CSN pins from nrf24l01
|
||||
cePin(4),
|
||||
csnPin(15),
|
||||
resetPin(0),
|
||||
ledPin(-2),
|
||||
radioInterfaceType(nRF24),
|
||||
packetRepeats(50),
|
||||
httpRepeatFactor(1),
|
||||
listenRepeats(3),
|
||||
discoveryPort(48899),
|
||||
simpleMqttClientStatus(false),
|
||||
stateFlushInterval(10000),
|
||||
mqttStateRateLimit(500),
|
||||
mqttDebounceDelay(500),
|
||||
mqttRetain(true),
|
||||
packetRepeatThrottleThreshold(200),
|
||||
packetRepeatThrottleSensitivity(0),
|
||||
packetRepeatMinimum(3),
|
||||
enableAutomaticModeSwitching(false),
|
||||
ledModeWifiConfig(LEDStatus::LEDMode::FastToggle),
|
||||
ledModeWifiFailed(LEDStatus::LEDMode::On),
|
||||
ledModeOperating(LEDStatus::LEDMode::SlowBlip),
|
||||
ledModePacket(LEDStatus::LEDMode::Flicker),
|
||||
ledModePacketCount(3),
|
||||
hostname("milight-hub"),
|
||||
rf24PowerLevel(RF24PowerLevelHelpers::defaultValue()),
|
||||
rf24Channels(RF24ChannelHelpers::allValues()),
|
||||
groupStateFields(DEFAULT_GROUP_STATE_FIELDS),
|
||||
rf24ListenChannel(RF24Channel::RF24_LOW),
|
||||
packetRepeatsPerLoop(10),
|
||||
wifiMode(WifiMode::N),
|
||||
defaultTransitionPeriod(500),
|
||||
_autoRestartPeriod(0)
|
||||
{ }
|
||||
|
||||
~Settings() { }
|
||||
|
||||
bool isAuthenticationEnabled() const;
|
||||
const String& getUsername() const;
|
||||
const String& getPassword() const;
|
||||
|
||||
bool isAutoRestartEnabled();
|
||||
size_t getAutoRestartPeriod();
|
||||
|
||||
static void load(Settings& settings);
|
||||
|
||||
static RadioInterfaceType typeFromString(const String& s);
|
||||
static String typeToString(RadioInterfaceType type);
|
||||
static std::vector<RF24Channel> defaultListenChannels();
|
||||
|
||||
void save();
|
||||
String toJson(const bool prettyPrint = true);
|
||||
void serialize(Print& stream, const bool prettyPrint = false);
|
||||
void updateDeviceIds(JsonArray arr);
|
||||
void updateGatewayConfigs(JsonArray arr);
|
||||
void patch(JsonObject obj);
|
||||
String mqttServer();
|
||||
uint16_t mqttPort();
|
||||
std::map<String, BulbId>::const_iterator findAlias(MiLightRemoteType deviceType, uint16_t deviceId, uint8_t groupId);
|
||||
|
||||
String adminUsername;
|
||||
String adminPassword;
|
||||
uint8_t cePin;
|
||||
uint8_t csnPin;
|
||||
uint8_t resetPin;
|
||||
int8_t ledPin;
|
||||
RadioInterfaceType radioInterfaceType;
|
||||
size_t packetRepeats;
|
||||
size_t httpRepeatFactor;
|
||||
uint8_t listenRepeats;
|
||||
uint16_t discoveryPort;
|
||||
String _mqttServer;
|
||||
String mqttUsername;
|
||||
String mqttPassword;
|
||||
String mqttTopicPattern;
|
||||
String mqttUpdateTopicPattern;
|
||||
String mqttStateTopicPattern;
|
||||
String mqttClientStatusTopic;
|
||||
bool simpleMqttClientStatus;
|
||||
size_t stateFlushInterval;
|
||||
size_t mqttStateRateLimit;
|
||||
size_t mqttDebounceDelay;
|
||||
bool mqttRetain;
|
||||
size_t packetRepeatThrottleThreshold;
|
||||
size_t packetRepeatThrottleSensitivity;
|
||||
size_t packetRepeatMinimum;
|
||||
bool enableAutomaticModeSwitching;
|
||||
LEDStatus::LEDMode ledModeWifiConfig;
|
||||
LEDStatus::LEDMode ledModeWifiFailed;
|
||||
LEDStatus::LEDMode ledModeOperating;
|
||||
LEDStatus::LEDMode ledModePacket;
|
||||
size_t ledModePacketCount;
|
||||
String hostname;
|
||||
RF24PowerLevel rf24PowerLevel;
|
||||
std::vector<uint16_t> deviceIds;
|
||||
std::vector<RF24Channel> rf24Channels;
|
||||
std::vector<GroupStateField> groupStateFields;
|
||||
std::vector<std::shared_ptr<GatewayConfig>> gatewayConfigs;
|
||||
RF24Channel rf24ListenChannel;
|
||||
String wifiStaticIP;
|
||||
String wifiStaticIPNetmask;
|
||||
String wifiStaticIPGateway;
|
||||
size_t packetRepeatsPerLoop;
|
||||
std::map<String, BulbId> groupIdAliases;
|
||||
std::map<uint32_t, BulbId> deletedGroupIdAliases;
|
||||
String homeAssistantDiscoveryPrefix;
|
||||
WifiMode wifiMode;
|
||||
uint16_t defaultTransitionPeriod;
|
||||
|
||||
protected:
|
||||
size_t _autoRestartPeriod;
|
||||
|
||||
void parseGroupIdAliases(JsonObject json);
|
||||
void dumpGroupIdAliases(JsonObject json);
|
||||
|
||||
static WifiMode wifiModeFromString(const String& mode);
|
||||
static String wifiModeToString(WifiMode mode);
|
||||
|
||||
template <typename T>
|
||||
void setIfPresent(JsonObject obj, const char* key, T& var) {
|
||||
if (obj.containsKey(key)) {
|
||||
JsonVariant val = obj[key];
|
||||
var = val.as<T>();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
29
lib/Settings/StringStream.h
Normal file
29
lib/Settings/StringStream.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Adapated from https://gist.github.com/cmaglie/5883185
|
||||
*/
|
||||
|
||||
#ifndef _STRING_STREAM_H_INCLUDED_
|
||||
#define _STRING_STREAM_H_INCLUDED_
|
||||
|
||||
#include <Stream.h>
|
||||
|
||||
class StringStream : public Stream
|
||||
{
|
||||
public:
|
||||
StringStream(String &s) : string(s), position(0) { }
|
||||
|
||||
// Stream methods
|
||||
virtual int available() { return string.length() - position; }
|
||||
virtual int read() { return position < string.length() ? string[position++] : -1; }
|
||||
virtual int peek() { return position < string.length() ? string[position] : -1; }
|
||||
virtual void flush() { };
|
||||
// Print methods
|
||||
virtual size_t write(uint8_t c) { string += (char)c; return 1; };
|
||||
|
||||
private:
|
||||
String &string;
|
||||
unsigned int length;
|
||||
unsigned int position;
|
||||
};
|
||||
|
||||
#endif // _STRING_STREAM_H_INCLUDED_
|
60
lib/Transitions/ChangeFieldOnFinishTransition.cpp
Normal file
60
lib/Transitions/ChangeFieldOnFinishTransition.cpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include <ChangeFieldOnFinishTransition.h>
|
||||
#include <MiLightStatus.h>
|
||||
|
||||
ChangeFieldOnFinishTransition::Builder::Builder(
|
||||
size_t id,
|
||||
GroupStateField field,
|
||||
uint16_t arg,
|
||||
std::shared_ptr<Transition::Builder> delegate
|
||||
)
|
||||
: Transition::Builder(delegate->id, delegate->defaultPeriod, delegate->bulbId, delegate->callback, delegate->getMaxSteps())
|
||||
, delegate(delegate)
|
||||
, field(field)
|
||||
, arg(arg)
|
||||
{ }
|
||||
|
||||
std::shared_ptr<Transition> ChangeFieldOnFinishTransition::Builder::_build() const {
|
||||
delegate->setDurationRaw(this->getOrComputeDuration());
|
||||
delegate->setPeriod(this->getOrComputePeriod());
|
||||
|
||||
return std::make_shared<ChangeFieldOnFinishTransition>(
|
||||
delegate->build(),
|
||||
field,
|
||||
arg,
|
||||
delegate->getPeriod()
|
||||
);
|
||||
}
|
||||
|
||||
ChangeFieldOnFinishTransition::ChangeFieldOnFinishTransition(
|
||||
std::shared_ptr<Transition> delegate,
|
||||
GroupStateField field,
|
||||
uint16_t arg,
|
||||
size_t period
|
||||
) : Transition(delegate->id, delegate->bulbId, period, delegate->callback)
|
||||
, delegate(delegate)
|
||||
, field(field)
|
||||
, arg(arg)
|
||||
, changeSent(false)
|
||||
{ }
|
||||
|
||||
bool ChangeFieldOnFinishTransition::isFinished() {
|
||||
return delegate->isFinished() && changeSent;
|
||||
}
|
||||
|
||||
void ChangeFieldOnFinishTransition::step() {
|
||||
if (! delegate->isFinished()) {
|
||||
delegate->step();
|
||||
} else {
|
||||
callback(bulbId, field, arg);
|
||||
changeSent = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ChangeFieldOnFinishTransition::childSerialize(JsonObject& json) {
|
||||
json[F("type")] = F("change_on_finish");
|
||||
json[F("field")] = GroupStateFieldHelpers::getFieldName(field);
|
||||
json[F("value")] = arg;
|
||||
|
||||
JsonObject child = json.createNestedObject(F("child"));
|
||||
delegate->childSerialize(child);
|
||||
}
|
37
lib/Transitions/ChangeFieldOnFinishTransition.h
Normal file
37
lib/Transitions/ChangeFieldOnFinishTransition.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
#include <Transition.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
class ChangeFieldOnFinishTransition : public Transition {
|
||||
public:
|
||||
|
||||
class Builder : public Transition::Builder {
|
||||
public:
|
||||
Builder(size_t id, GroupStateField field, uint16_t arg, std::shared_ptr<Transition::Builder> delgate);
|
||||
|
||||
virtual std::shared_ptr<Transition> _build() const override;
|
||||
|
||||
private:
|
||||
const std::shared_ptr<Transition::Builder> delegate;
|
||||
const GroupStateField field;
|
||||
const uint16_t arg;
|
||||
};
|
||||
|
||||
ChangeFieldOnFinishTransition(
|
||||
std::shared_ptr<Transition> delegate,
|
||||
GroupStateField field,
|
||||
uint16_t arg,
|
||||
size_t period
|
||||
);
|
||||
|
||||
virtual bool isFinished() override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Transition> delegate;
|
||||
const GroupStateField field;
|
||||
const uint16_t arg;
|
||||
bool changeSent;
|
||||
|
||||
virtual void step() override;
|
||||
virtual void childSerialize(JsonObject& json) override;
|
||||
};
|
156
lib/Transitions/ColorTransition.cpp
Normal file
156
lib/Transitions/ColorTransition.cpp
Normal file
|
@ -0,0 +1,156 @@
|
|||
#include <ColorTransition.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
ColorTransition::Builder::Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, const ParsedColor& start, const ParsedColor& end)
|
||||
: Transition::Builder(id, defaultPeriod, bulbId, callback, calculateMaxDistance(start, end))
|
||||
, start(start)
|
||||
, end(end)
|
||||
{ }
|
||||
|
||||
std::shared_ptr<Transition> ColorTransition::Builder::_build() const {
|
||||
size_t duration = getOrComputeDuration();
|
||||
size_t numPeriods = getOrComputeNumPeriods();
|
||||
size_t period = getOrComputePeriod();
|
||||
|
||||
int16_t dr = end.r - start.r
|
||||
, dg = end.g - start.g
|
||||
, db = end.b - start.b;
|
||||
|
||||
RgbColor stepSizes(
|
||||
calculateStepSizePart(dr, duration, period),
|
||||
calculateStepSizePart(dg, duration, period),
|
||||
calculateStepSizePart(db, duration, period)
|
||||
);
|
||||
|
||||
return std::make_shared<ColorTransition>(
|
||||
id,
|
||||
bulbId,
|
||||
start,
|
||||
end,
|
||||
stepSizes,
|
||||
duration,
|
||||
period,
|
||||
numPeriods,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
ColorTransition::RgbColor::RgbColor()
|
||||
: r(0)
|
||||
, g(0)
|
||||
, b(0)
|
||||
{ }
|
||||
|
||||
ColorTransition::RgbColor::RgbColor(const ParsedColor& color)
|
||||
: r(color.r)
|
||||
, g(color.g)
|
||||
, b(color.b)
|
||||
{ }
|
||||
|
||||
ColorTransition::RgbColor::RgbColor(int16_t r, int16_t g, int16_t b)
|
||||
: r(r)
|
||||
, g(g)
|
||||
, b(b)
|
||||
{ }
|
||||
|
||||
bool ColorTransition::RgbColor::operator==(const RgbColor& other) {
|
||||
return r == other.r && g == other.g && b == other.b;
|
||||
}
|
||||
|
||||
ColorTransition::ColorTransition(
|
||||
size_t id,
|
||||
const BulbId& bulbId,
|
||||
const ParsedColor& startColor,
|
||||
const ParsedColor& endColor,
|
||||
RgbColor stepSizes,
|
||||
size_t duration,
|
||||
size_t period,
|
||||
size_t numPeriods,
|
||||
TransitionFn callback
|
||||
) : Transition(id, bulbId, period, callback)
|
||||
, endColor(endColor)
|
||||
, currentColor(startColor)
|
||||
, stepSizes(stepSizes)
|
||||
, lastHue(400) // use impossible values to force a packet send
|
||||
, lastSaturation(200)
|
||||
, finished(false)
|
||||
{
|
||||
int16_t dr = endColor.r - startColor.r
|
||||
, dg = endColor.g - startColor.g
|
||||
, db = endColor.b - startColor.b;
|
||||
// Calculate step sizes in terms of the period
|
||||
stepSizes.r = calculateStepSizePart(dr, duration, period);
|
||||
stepSizes.g = calculateStepSizePart(dg, duration, period);
|
||||
stepSizes.b = calculateStepSizePart(db, duration, period);
|
||||
}
|
||||
|
||||
size_t ColorTransition::calculateMaxDistance(const ParsedColor& start, const ParsedColor& end) {
|
||||
int16_t dr = end.r - start.r
|
||||
, dg = end.g - start.g
|
||||
, db = end.b - start.b;
|
||||
|
||||
int16_t max = std::max(std::max(dr, dg), db);
|
||||
int16_t min = std::min(std::min(dr, dg), db);
|
||||
int16_t maxAbs = std::abs(min) > std::abs(max) ? min : max;
|
||||
|
||||
return maxAbs;
|
||||
}
|
||||
|
||||
size_t ColorTransition::calculateColorPeriod(ColorTransition* t, const ParsedColor& start, const ParsedColor& end, size_t stepSize, size_t duration) {
|
||||
return Transition::calculatePeriod(calculateMaxDistance(start, end), stepSize, duration);
|
||||
}
|
||||
|
||||
int16_t ColorTransition::calculateStepSizePart(int16_t distance, size_t duration, size_t period) {
|
||||
double stepSize = (distance / static_cast<double>(duration)) * period;
|
||||
int16_t rounded = std::ceil(std::abs(stepSize));
|
||||
|
||||
if (distance < 0) {
|
||||
rounded = -rounded;
|
||||
}
|
||||
|
||||
return rounded;
|
||||
}
|
||||
|
||||
void ColorTransition::step() {
|
||||
ParsedColor parsedColor = ParsedColor::fromRgb(currentColor.r, currentColor.g, currentColor.b);
|
||||
|
||||
if (parsedColor.hue != lastHue) {
|
||||
callback(bulbId, GroupStateField::HUE, parsedColor.hue);
|
||||
lastHue = parsedColor.hue;
|
||||
}
|
||||
if (parsedColor.saturation != lastSaturation) {
|
||||
callback(bulbId, GroupStateField::SATURATION, parsedColor.saturation);
|
||||
lastSaturation = parsedColor.saturation;
|
||||
}
|
||||
|
||||
if (currentColor == endColor) {
|
||||
finished = true;
|
||||
} else {
|
||||
Transition::stepValue(currentColor.r, endColor.r, stepSizes.r);
|
||||
Transition::stepValue(currentColor.g, endColor.g, stepSizes.g);
|
||||
Transition::stepValue(currentColor.b, endColor.b, stepSizes.b);
|
||||
}
|
||||
}
|
||||
|
||||
bool ColorTransition::isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
void ColorTransition::childSerialize(JsonObject& json) {
|
||||
json[F("type")] = F("color");
|
||||
|
||||
JsonArray currentColorArr = json.createNestedArray(F("current_color"));
|
||||
currentColorArr.add(currentColor.r);
|
||||
currentColorArr.add(currentColor.g);
|
||||
currentColorArr.add(currentColor.b);
|
||||
|
||||
JsonArray endColorArr = json.createNestedArray(F("end_color"));
|
||||
endColorArr.add(endColor.r);
|
||||
endColorArr.add(endColor.g);
|
||||
endColorArr.add(endColor.b);
|
||||
|
||||
JsonArray stepSizesArr = json.createNestedArray(F("step_sizes"));
|
||||
stepSizesArr.add(stepSizes.r);
|
||||
stepSizesArr.add(stepSizes.g);
|
||||
stepSizesArr.add(stepSizes.b);
|
||||
}
|
59
lib/Transitions/ColorTransition.h
Normal file
59
lib/Transitions/ColorTransition.h
Normal file
|
@ -0,0 +1,59 @@
|
|||
#include <Transition.h>
|
||||
#include <ParsedColor.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
class ColorTransition : public Transition {
|
||||
public:
|
||||
struct RgbColor {
|
||||
RgbColor();
|
||||
RgbColor(const ParsedColor& color);
|
||||
RgbColor(int16_t r, int16_t g, int16_t b);
|
||||
bool operator==(const RgbColor& other);
|
||||
|
||||
int16_t r, g, b;
|
||||
};
|
||||
|
||||
class Builder : public Transition::Builder {
|
||||
public:
|
||||
Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, const ParsedColor& start, const ParsedColor& end);
|
||||
|
||||
virtual std::shared_ptr<Transition> _build() const override;
|
||||
|
||||
private:
|
||||
const ParsedColor& start;
|
||||
const ParsedColor& end;
|
||||
RgbColor stepSizes;
|
||||
};
|
||||
|
||||
ColorTransition(
|
||||
size_t id,
|
||||
const BulbId& bulbId,
|
||||
const ParsedColor& startColor,
|
||||
const ParsedColor& endColor,
|
||||
RgbColor stepSizes,
|
||||
size_t duration,
|
||||
size_t period,
|
||||
size_t numPeriods,
|
||||
TransitionFn callback
|
||||
);
|
||||
|
||||
static size_t calculateColorPeriod(ColorTransition* t, const ParsedColor& start, const ParsedColor& end, size_t stepSize, size_t duration);
|
||||
inline static size_t calculateMaxDistance(const ParsedColor& start, const ParsedColor& end);
|
||||
inline static int16_t calculateStepSizePart(int16_t distance, size_t duration, size_t period);
|
||||
virtual bool isFinished() override;
|
||||
|
||||
protected:
|
||||
const RgbColor endColor;
|
||||
RgbColor currentColor;
|
||||
RgbColor stepSizes;
|
||||
|
||||
// Store these to avoid wasted packets
|
||||
uint16_t lastHue;
|
||||
uint16_t lastSaturation;
|
||||
bool finished;
|
||||
|
||||
virtual void step() override;
|
||||
virtual void childSerialize(JsonObject& json) override;
|
||||
static inline void stepPart(uint16_t& current, uint16_t end, int16_t step);
|
||||
};
|
85
lib/Transitions/FieldTransition.cpp
Normal file
85
lib/Transitions/FieldTransition.cpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
#include <FieldTransition.h>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
FieldTransition::Builder::Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, GroupStateField field, uint16_t start, uint16_t end)
|
||||
: Transition::Builder(
|
||||
id,
|
||||
defaultPeriod,
|
||||
bulbId,
|
||||
callback,
|
||||
max(
|
||||
static_cast<size_t>(1),
|
||||
static_cast<size_t>(std::abs(static_cast<int16_t>(end) - static_cast<uint16_t>(start)))
|
||||
)
|
||||
)
|
||||
, stepSize(0)
|
||||
, field(field)
|
||||
, start(start)
|
||||
, end(end)
|
||||
{ }
|
||||
|
||||
std::shared_ptr<Transition> FieldTransition::Builder::_build() const {
|
||||
size_t numPeriods = getOrComputeNumPeriods();
|
||||
size_t period = getOrComputePeriod();
|
||||
|
||||
int16_t distance = end - start;
|
||||
int16_t stepSize = ceil(std::abs(distance / static_cast<float>(numPeriods)));
|
||||
|
||||
if (end < start) {
|
||||
stepSize = -stepSize;
|
||||
}
|
||||
if (stepSize == 0) {
|
||||
stepSize = end > start ? 1 : -1;
|
||||
}
|
||||
|
||||
return std::make_shared<FieldTransition>(
|
||||
id,
|
||||
bulbId,
|
||||
field,
|
||||
start,
|
||||
end,
|
||||
stepSize,
|
||||
period,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
FieldTransition::FieldTransition(
|
||||
size_t id,
|
||||
const BulbId& bulbId,
|
||||
GroupStateField field,
|
||||
uint16_t startValue,
|
||||
uint16_t endValue,
|
||||
int16_t stepSize,
|
||||
size_t period,
|
||||
TransitionFn callback
|
||||
) : Transition(id, bulbId, period, callback)
|
||||
, field(field)
|
||||
, currentValue(startValue)
|
||||
, endValue(endValue)
|
||||
, stepSize(stepSize)
|
||||
, finished(false)
|
||||
{ }
|
||||
|
||||
void FieldTransition::step() {
|
||||
callback(bulbId, field, currentValue);
|
||||
|
||||
if (currentValue != endValue) {
|
||||
Transition::stepValue(currentValue, endValue, stepSize);
|
||||
} else {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool FieldTransition::isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
void FieldTransition::childSerialize(JsonObject& json) {
|
||||
json[F("type")] = F("field");
|
||||
json[F("field")] = GroupStateFieldHelpers::getFieldName(field);
|
||||
json[F("current_value")] = currentValue;
|
||||
json[F("end_value")] = endValue;
|
||||
json[F("step_size")] = stepSize;
|
||||
}
|
48
lib/Transitions/FieldTransition.h
Normal file
48
lib/Transitions/FieldTransition.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
#include <GroupStateField.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
#include <Transition.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
class FieldTransition : public Transition {
|
||||
public:
|
||||
|
||||
class Builder : public Transition::Builder {
|
||||
public:
|
||||
Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, GroupStateField field, uint16_t start, uint16_t end);
|
||||
|
||||
virtual std::shared_ptr<Transition> _build() const override;
|
||||
|
||||
private:
|
||||
size_t stepSize;
|
||||
GroupStateField field;
|
||||
uint16_t start;
|
||||
uint16_t end;
|
||||
};
|
||||
|
||||
FieldTransition(
|
||||
size_t id,
|
||||
const BulbId& bulbId,
|
||||
GroupStateField field,
|
||||
uint16_t startValue,
|
||||
uint16_t endValue,
|
||||
int16_t stepSize,
|
||||
size_t period,
|
||||
TransitionFn callback
|
||||
);
|
||||
|
||||
virtual bool isFinished() override;
|
||||
|
||||
private:
|
||||
const GroupStateField field;
|
||||
int16_t currentValue;
|
||||
const int16_t endValue;
|
||||
const int16_t stepSize;
|
||||
bool finished;
|
||||
|
||||
virtual void step() override;
|
||||
virtual void childSerialize(JsonObject& json) override;
|
||||
};
|
179
lib/Transitions/Transition.cpp
Normal file
179
lib/Transitions/Transition.cpp
Normal file
|
@ -0,0 +1,179 @@
|
|||
#include <Transition.h>
|
||||
#include <Arduino.h>
|
||||
#include <cmath>
|
||||
|
||||
Transition::Builder::Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, size_t maxSteps)
|
||||
: id(id)
|
||||
, defaultPeriod(defaultPeriod)
|
||||
, bulbId(bulbId)
|
||||
, callback(callback)
|
||||
, duration(0)
|
||||
, period(0)
|
||||
, numPeriods(0)
|
||||
, maxSteps(maxSteps)
|
||||
{ }
|
||||
|
||||
Transition::Builder& Transition::Builder::setDuration(float duration) {
|
||||
this->duration = duration * DURATION_UNIT_MULTIPLIER;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Transition::Builder::setDurationRaw(size_t duration) {
|
||||
this->duration = duration;
|
||||
}
|
||||
|
||||
Transition::Builder& Transition::Builder::setPeriod(size_t period) {
|
||||
this->period = period;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Transition::Builder& Transition::Builder::setDurationAwarePeriod(size_t period, size_t duration, size_t maxSteps) {
|
||||
if ((period * maxSteps) < duration) {
|
||||
setPeriod(std::ceil(duration / static_cast<float>(maxSteps)));
|
||||
} else {
|
||||
setPeriod(period);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getNumPeriods() const {
|
||||
return this->numPeriods;
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getDuration() const {
|
||||
return this->duration;
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getPeriod() const {
|
||||
return this->period;
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getMaxSteps() const {
|
||||
return this->maxSteps;
|
||||
}
|
||||
|
||||
bool Transition::Builder::isSetDuration() const {
|
||||
return this->duration > 0;
|
||||
}
|
||||
|
||||
bool Transition::Builder::isSetPeriod() const {
|
||||
return this->period > 0;
|
||||
}
|
||||
|
||||
bool Transition::Builder::isSetNumPeriods() const {
|
||||
return this->numPeriods > 0;
|
||||
}
|
||||
|
||||
size_t Transition::Builder::numSetParams() const {
|
||||
size_t setCount = 0;
|
||||
|
||||
if (isSetDuration()) { ++setCount; }
|
||||
if (isSetPeriod()) { ++setCount; }
|
||||
if (isSetNumPeriods()) { ++setCount; }
|
||||
|
||||
return setCount;
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getOrComputePeriod() const {
|
||||
if (period > 0) {
|
||||
return period;
|
||||
} else if (duration > 0 && numPeriods > 0) {
|
||||
size_t computed = floor(duration / static_cast<float>(numPeriods));
|
||||
return max(MIN_PERIOD, computed);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getOrComputeDuration() const {
|
||||
if (duration > 0) {
|
||||
return duration;
|
||||
} else if (period > 0 && numPeriods > 0) {
|
||||
return period * numPeriods;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t Transition::Builder::getOrComputeNumPeriods() const {
|
||||
if (numPeriods > 0) {
|
||||
return numPeriods;
|
||||
} else if (period > 0 && duration > 0) {
|
||||
size_t _numPeriods = ceil(duration / static_cast<float>(period));
|
||||
return max(static_cast<size_t>(1), _numPeriods);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Transition> Transition::Builder::build() {
|
||||
// Set defaults for underspecified transitions
|
||||
size_t numSet = numSetParams();
|
||||
|
||||
if (numSet == 0) {
|
||||
setDuration(DEFAULT_DURATION);
|
||||
setDurationAwarePeriod(defaultPeriod, duration, maxSteps);
|
||||
} else if (numSet == 1) {
|
||||
// If duration is unbound, bind it
|
||||
if (! isSetDuration()) {
|
||||
setDurationRaw(DEFAULT_DURATION);
|
||||
// Otherwise, bind the period
|
||||
} else {
|
||||
setDurationAwarePeriod(defaultPeriod, duration, maxSteps);
|
||||
}
|
||||
}
|
||||
|
||||
return _build();
|
||||
}
|
||||
|
||||
Transition::Transition(
|
||||
size_t id,
|
||||
const BulbId& bulbId,
|
||||
size_t period,
|
||||
TransitionFn callback
|
||||
) : id(id)
|
||||
, bulbId(bulbId)
|
||||
, callback(callback)
|
||||
, period(period)
|
||||
, lastSent(0)
|
||||
{ }
|
||||
|
||||
void Transition::tick() {
|
||||
unsigned long now = millis();
|
||||
|
||||
if ((lastSent + period) <= now
|
||||
&& ((!isFinished() || lastSent == 0))) { // always send at least once
|
||||
|
||||
step();
|
||||
lastSent = now;
|
||||
}
|
||||
}
|
||||
|
||||
size_t Transition::calculatePeriod(int16_t distance, size_t stepSize, size_t duration) {
|
||||
float fPeriod =
|
||||
distance != 0
|
||||
? (duration / (distance / static_cast<float>(stepSize)))
|
||||
: 0;
|
||||
|
||||
return static_cast<size_t>(round(fPeriod));
|
||||
}
|
||||
|
||||
void Transition::stepValue(int16_t& current, int16_t end, int16_t stepSize) {
|
||||
int16_t delta = end - current;
|
||||
if (std::abs(delta) < std::abs(stepSize)) {
|
||||
current += delta;
|
||||
} else {
|
||||
current += stepSize;
|
||||
}
|
||||
}
|
||||
|
||||
void Transition::serialize(JsonObject& json) {
|
||||
json[F("id")] = id;
|
||||
json[F("period")] = period;
|
||||
json[F("last_sent")] = lastSent;
|
||||
|
||||
JsonObject bulbParams = json.createNestedObject("bulb");
|
||||
bulbId.serialize(bulbParams);
|
||||
|
||||
childSerialize(json);
|
||||
}
|
99
lib/Transitions/Transition.h
Normal file
99
lib/Transitions/Transition.h
Normal file
|
@ -0,0 +1,99 @@
|
|||
#include <BulbId.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <GroupStateField.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#pragma once
|
||||
|
||||
class Transition {
|
||||
public:
|
||||
using TransitionFn = std::function<void(const BulbId& bulbId, GroupStateField field, uint16_t value)>;
|
||||
|
||||
// transition commands are in seconds, convert to ms.
|
||||
static const uint16_t DURATION_UNIT_MULTIPLIER = 1000;
|
||||
|
||||
|
||||
class Builder {
|
||||
public:
|
||||
Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, size_t maxSteps);
|
||||
|
||||
Builder& setDuration(float duration);
|
||||
Builder& setPeriod(size_t period);
|
||||
|
||||
/**
|
||||
* Users are typically defining transitions using:
|
||||
* 1. The desired end state (and implicitly the start state, assumed to be current)
|
||||
* 2. The duraiton
|
||||
* The user only cares about the period to the degree that it affects the smoothness of
|
||||
* the transition.
|
||||
*
|
||||
* For example, if the user wants to throttle brightness from 0 -> 100 over 5min, the
|
||||
* default period is going to be way too short to enable that. So we need to force the
|
||||
* period to be longer to fit the duration.
|
||||
*/
|
||||
Builder& setDurationAwarePeriod(size_t desiredPeriod, size_t duration, size_t maxSteps);
|
||||
|
||||
void setDurationRaw(size_t duration);
|
||||
|
||||
bool isSetDuration() const;
|
||||
bool isSetPeriod() const;
|
||||
bool isSetNumPeriods() const;
|
||||
|
||||
size_t getOrComputePeriod() const;
|
||||
size_t getOrComputeDuration() const;
|
||||
size_t getOrComputeNumPeriods() const;
|
||||
|
||||
size_t getDuration() const;
|
||||
size_t getPeriod() const;
|
||||
size_t getNumPeriods() const;
|
||||
size_t getMaxSteps() const;
|
||||
|
||||
std::shared_ptr<Transition> build();
|
||||
|
||||
const size_t id;
|
||||
const uint16_t defaultPeriod;
|
||||
const BulbId& bulbId;
|
||||
const TransitionFn callback;
|
||||
|
||||
private:
|
||||
size_t duration;
|
||||
size_t period;
|
||||
size_t numPeriods;
|
||||
size_t maxSteps;
|
||||
|
||||
virtual std::shared_ptr<Transition> _build() const = 0;
|
||||
size_t numSetParams() const;
|
||||
};
|
||||
|
||||
// If period goes lower than this, throttle other parameters up to adjust.
|
||||
static const size_t MIN_PERIOD = 150;
|
||||
static const size_t DEFAULT_DURATION = 10000;
|
||||
|
||||
const size_t id;
|
||||
const BulbId bulbId;
|
||||
const TransitionFn callback;
|
||||
|
||||
Transition(
|
||||
size_t id,
|
||||
const BulbId& bulbId,
|
||||
size_t period,
|
||||
TransitionFn callback
|
||||
);
|
||||
|
||||
void tick();
|
||||
virtual bool isFinished() = 0;
|
||||
void serialize(JsonObject& doc);
|
||||
virtual void step() = 0;
|
||||
virtual void childSerialize(JsonObject& doc) = 0;
|
||||
|
||||
static size_t calculatePeriod(int16_t distance, size_t stepSize, size_t duration);
|
||||
|
||||
protected:
|
||||
const size_t period;
|
||||
unsigned long lastSent;
|
||||
|
||||
static void stepValue(int16_t& current, int16_t end, int16_t stepSize);
|
||||
};
|
142
lib/Transitions/TransitionController.cpp
Normal file
142
lib/Transitions/TransitionController.cpp
Normal file
|
@ -0,0 +1,142 @@
|
|||
#include <Transition.h>
|
||||
#include <FieldTransition.h>
|
||||
#include <ColorTransition.h>
|
||||
#include <ChangeFieldOnFinishTransition.h>
|
||||
#include <GroupStateField.h>
|
||||
#include <MiLightStatus.h>
|
||||
|
||||
#include <TransitionController.h>
|
||||
#include <LinkedList.h>
|
||||
#include <functional>
|
||||
|
||||
using namespace std::placeholders;
|
||||
|
||||
TransitionController::TransitionController()
|
||||
: callback(std::bind(&TransitionController::transitionCallback, this, _1, _2, _3))
|
||||
, currentId(0)
|
||||
, defaultPeriod(500)
|
||||
{ }
|
||||
|
||||
void TransitionController::setDefaultPeriod(uint16_t defaultPeriod) {
|
||||
this->defaultPeriod = defaultPeriod;
|
||||
}
|
||||
|
||||
void TransitionController::clearListeners() {
|
||||
observers.clear();
|
||||
}
|
||||
|
||||
void TransitionController::addListener(Transition::TransitionFn fn) {
|
||||
observers.push_back(fn);
|
||||
}
|
||||
|
||||
std::shared_ptr<Transition::Builder> TransitionController::buildColorTransition(const BulbId& bulbId, const ParsedColor& start, const ParsedColor& end) {
|
||||
return std::make_shared<ColorTransition::Builder>(
|
||||
currentId++,
|
||||
defaultPeriod,
|
||||
bulbId,
|
||||
callback,
|
||||
start,
|
||||
end
|
||||
);
|
||||
}
|
||||
|
||||
std::shared_ptr<Transition::Builder> TransitionController::buildFieldTransition(const BulbId& bulbId, GroupStateField field, uint16_t start, uint16_t end) {
|
||||
return std::make_shared<FieldTransition::Builder>(
|
||||
currentId++,
|
||||
defaultPeriod,
|
||||
bulbId,
|
||||
callback,
|
||||
field,
|
||||
start,
|
||||
end
|
||||
);
|
||||
}
|
||||
|
||||
std::shared_ptr<Transition::Builder> TransitionController::buildStatusTransition(const BulbId& bulbId, MiLightStatus status, uint8_t startLevel) {
|
||||
std::shared_ptr<Transition::Builder> transition;
|
||||
|
||||
if (status == ON) {
|
||||
// Make sure bulb is on before transitioning brightness
|
||||
callback(bulbId, GroupStateField::STATUS, ON);
|
||||
|
||||
transition = buildFieldTransition(bulbId, GroupStateField::LEVEL, startLevel, 100);
|
||||
} else {
|
||||
transition = std::make_shared<ChangeFieldOnFinishTransition::Builder>(
|
||||
currentId++,
|
||||
GroupStateField::STATUS,
|
||||
OFF,
|
||||
buildFieldTransition(bulbId, GroupStateField::LEVEL, startLevel, 0)
|
||||
);
|
||||
}
|
||||
|
||||
return transition;
|
||||
}
|
||||
|
||||
void TransitionController::addTransition(std::shared_ptr<Transition> transition) {
|
||||
activeTransitions.add(transition);
|
||||
}
|
||||
|
||||
void TransitionController::transitionCallback(const BulbId& bulbId, GroupStateField field, uint16_t arg) {
|
||||
for (auto it = observers.begin(); it != observers.end(); ++it) {
|
||||
(*it)(bulbId, field, arg);
|
||||
}
|
||||
}
|
||||
|
||||
void TransitionController::clear() {
|
||||
activeTransitions.clear();
|
||||
}
|
||||
|
||||
void TransitionController::loop() {
|
||||
auto current = activeTransitions.getHead();
|
||||
|
||||
while (current != nullptr) {
|
||||
auto next = current->next;
|
||||
|
||||
Transition& t = *current->data;
|
||||
t.tick();
|
||||
|
||||
if (t.isFinished()) {
|
||||
activeTransitions.remove(current);
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
ListNode<std::shared_ptr<Transition>>* TransitionController::getTransitions() {
|
||||
return activeTransitions.getHead();
|
||||
}
|
||||
|
||||
ListNode<std::shared_ptr<Transition>>* TransitionController::findTransition(size_t id) {
|
||||
auto current = getTransitions();
|
||||
|
||||
while (current != nullptr) {
|
||||
if (current->data->id == id) {
|
||||
return current;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Transition* TransitionController::getTransition(size_t id) {
|
||||
auto node = findTransition(id);
|
||||
|
||||
if (node == nullptr) {
|
||||
return nullptr;
|
||||
} else {
|
||||
return node->data.get();
|
||||
}
|
||||
}
|
||||
|
||||
bool TransitionController::deleteTransition(size_t id) {
|
||||
auto node = findTransition(id);
|
||||
|
||||
if (node == nullptr) {
|
||||
return false;
|
||||
} else {
|
||||
activeTransitions.remove(node);
|
||||
return true;
|
||||
}
|
||||
}
|
39
lib/Transitions/TransitionController.h
Normal file
39
lib/Transitions/TransitionController.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
#include <Transition.h>
|
||||
#include <LinkedList.h>
|
||||
#include <ParsedColor.h>
|
||||
#include <GroupStateField.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#pragma once
|
||||
|
||||
class TransitionController {
|
||||
public:
|
||||
TransitionController();
|
||||
|
||||
void clearListeners();
|
||||
void addListener(Transition::TransitionFn fn);
|
||||
void setDefaultPeriod(uint16_t period);
|
||||
|
||||
std::shared_ptr<Transition::Builder> buildColorTransition(const BulbId& bulbId, const ParsedColor& start, const ParsedColor& end);
|
||||
std::shared_ptr<Transition::Builder> buildFieldTransition(const BulbId& bulbId, GroupStateField field, uint16_t start, uint16_t end);
|
||||
std::shared_ptr<Transition::Builder> buildStatusTransition(const BulbId& bulbId, MiLightStatus toStatus, uint8_t startLevel);
|
||||
|
||||
void addTransition(std::shared_ptr<Transition> transition);
|
||||
void clear();
|
||||
void loop();
|
||||
|
||||
ListNode<std::shared_ptr<Transition>>* getTransitions();
|
||||
Transition* getTransition(size_t id);
|
||||
ListNode<std::shared_ptr<Transition>>* findTransition(size_t id);
|
||||
bool deleteTransition(size_t id);
|
||||
|
||||
private:
|
||||
Transition::TransitionFn callback;
|
||||
LinkedList<std::shared_ptr<Transition>> activeTransitions;
|
||||
std::vector<Transition::TransitionFn> observers;
|
||||
size_t currentId;
|
||||
uint16_t defaultPeriod;
|
||||
|
||||
void transitionCallback(const BulbId& bulbId, GroupStateField field, uint16_t arg);
|
||||
};
|
60
lib/Types/BulbId.cpp
Normal file
60
lib/Types/BulbId.cpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include <BulbId.h>
|
||||
#include <GroupStateField.h>
|
||||
|
||||
BulbId::BulbId()
|
||||
: deviceId(0),
|
||||
groupId(0),
|
||||
deviceType(REMOTE_TYPE_UNKNOWN)
|
||||
{ }
|
||||
|
||||
BulbId::BulbId(const BulbId &other)
|
||||
: deviceId(other.deviceId),
|
||||
groupId(other.groupId),
|
||||
deviceType(other.deviceType)
|
||||
{ }
|
||||
|
||||
BulbId::BulbId(
|
||||
const uint16_t deviceId, const uint8_t groupId, const MiLightRemoteType deviceType
|
||||
)
|
||||
: deviceId(deviceId),
|
||||
groupId(groupId),
|
||||
deviceType(deviceType)
|
||||
{ }
|
||||
|
||||
void BulbId::operator=(const BulbId &other) {
|
||||
deviceId = other.deviceId;
|
||||
groupId = other.groupId;
|
||||
deviceType = other.deviceType;
|
||||
}
|
||||
|
||||
// determine if now BulbId's are the same. This compared deviceID (the controller/remote ID) and
|
||||
// groupId (the group number on the controller, 1-4 or 1-8 depending), but ignores the deviceType
|
||||
// (type of controller/remote) as this doesn't directly affect the identity of the bulb
|
||||
bool BulbId::operator==(const BulbId &other) {
|
||||
return deviceId == other.deviceId
|
||||
&& groupId == other.groupId
|
||||
&& deviceType == other.deviceType;
|
||||
}
|
||||
|
||||
uint32_t BulbId::getCompactId() const {
|
||||
uint32_t id = (deviceId << 24) | (deviceType << 8) | groupId;
|
||||
return id;
|
||||
}
|
||||
|
||||
String BulbId::getHexDeviceId() const {
|
||||
char hexDeviceId[7];
|
||||
sprintf_P(hexDeviceId, PSTR("0x%X"), deviceId);
|
||||
return hexDeviceId;
|
||||
}
|
||||
|
||||
void BulbId::serialize(JsonObject json) const {
|
||||
json[GroupStateFieldNames::DEVICE_ID] = deviceId;
|
||||
json[GroupStateFieldNames::GROUP_ID] = groupId;
|
||||
json[GroupStateFieldNames::DEVICE_TYPE] = MiLightRemoteTypeHelpers::remoteTypeToString(deviceType);
|
||||
}
|
||||
|
||||
void BulbId::serialize(JsonArray json) const {
|
||||
json.add(deviceId);
|
||||
json.add(MiLightRemoteTypeHelpers::remoteTypeToString(deviceType));
|
||||
json.add(groupId);
|
||||
}
|
22
lib/Types/BulbId.h
Normal file
22
lib/Types/BulbId.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <MiLightRemoteType.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
struct BulbId {
|
||||
uint16_t deviceId;
|
||||
uint8_t groupId;
|
||||
MiLightRemoteType deviceType;
|
||||
|
||||
BulbId();
|
||||
BulbId(const BulbId& other);
|
||||
BulbId(const uint16_t deviceId, const uint8_t groupId, const MiLightRemoteType deviceType);
|
||||
bool operator==(const BulbId& other);
|
||||
void operator=(const BulbId& other);
|
||||
|
||||
uint32_t getCompactId() const;
|
||||
String getHexDeviceId() const;
|
||||
void serialize(JsonObject json) const;
|
||||
void serialize(JsonArray json) const;
|
||||
};
|
52
lib/Types/GroupStateField.cpp
Normal file
52
lib/Types/GroupStateField.cpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
#include <GroupStateField.h>
|
||||
#include <Size.h>
|
||||
|
||||
static const char* STATE_NAMES[] = {
|
||||
GroupStateFieldNames::UNKNOWN,
|
||||
GroupStateFieldNames::STATE,
|
||||
GroupStateFieldNames::STATUS,
|
||||
GroupStateFieldNames::BRIGHTNESS,
|
||||
GroupStateFieldNames::LEVEL,
|
||||
GroupStateFieldNames::HUE,
|
||||
GroupStateFieldNames::SATURATION,
|
||||
GroupStateFieldNames::COLOR,
|
||||
GroupStateFieldNames::MODE,
|
||||
GroupStateFieldNames::KELVIN,
|
||||
GroupStateFieldNames::COLOR_TEMP,
|
||||
GroupStateFieldNames::BULB_MODE,
|
||||
GroupStateFieldNames::COMPUTED_COLOR,
|
||||
GroupStateFieldNames::EFFECT,
|
||||
GroupStateFieldNames::DEVICE_ID,
|
||||
GroupStateFieldNames::GROUP_ID,
|
||||
GroupStateFieldNames::DEVICE_TYPE,
|
||||
GroupStateFieldNames::OH_COLOR,
|
||||
GroupStateFieldNames::HEX_COLOR
|
||||
};
|
||||
|
||||
GroupStateField GroupStateFieldHelpers::getFieldByName(const char* name) {
|
||||
for (size_t i = 0; i < size(STATE_NAMES); i++) {
|
||||
if (0 == strcmp(name, STATE_NAMES[i])) {
|
||||
return static_cast<GroupStateField>(i);
|
||||
}
|
||||
}
|
||||
return GroupStateField::UNKNOWN;
|
||||
}
|
||||
|
||||
const char* GroupStateFieldHelpers::getFieldName(GroupStateField field) {
|
||||
for (size_t i = 0; i < size(STATE_NAMES); i++) {
|
||||
if (field == static_cast<GroupStateField>(i)) {
|
||||
return STATE_NAMES[i];
|
||||
}
|
||||
}
|
||||
return STATE_NAMES[0];
|
||||
}
|
||||
|
||||
bool GroupStateFieldHelpers::isBrightnessField(GroupStateField field) {
|
||||
switch (field) {
|
||||
case GroupStateField::BRIGHTNESS:
|
||||
case GroupStateField::LEVEL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue