Add new actuator : PWM controller (#737)
* Initial generalization from a PWM LED controller, to a PWM controller. Allow the config file to set the name of each channel and the number of channels. Fade time is now per-channel, so MQTT messages can genuinely set an individual channel, and specify the fade time for that channel. The default gamma is configurable in the config.
This commit is contained in:
1 parent
7b9944d1ca
commit
ef39e14497
6 files changed
+374
-3
No files matched your search
@@ -31,7 +31,46 @@ ON command
|
||||
Theoreticaly it should be possible to use every free IO pin. But after some tests only pin D2 works at WEMOS D1. Other platforms can work.
|
||||
The default setting use NEOPIXEL (WS2812B). The simplest wiring is direct connect D2 to data pin of LED stripe and connect VCC/GND to power source. You should also add an capacitor.
|
||||
|
||||
## PWM
|
||||
This module allows control over PWM outputs.
|
||||
It's primary use is for controlling LEDs, but it should be equally at home controlling anything that's controlled using PWM.
|
||||
E.g. LEDs, servos, PC fans.
|
||||
You would typically connect a PWM output to a transistor or MOSFET to allow control over higher power devices.
|
||||
|
||||
* JSON message format allows you to set any or all channels in a single message.
|
||||
* Each channel can be set to smoothly transition from its current setting to the new setting over a specified number of seconds.
|
||||
* Each channel can be calibrated with min and max settings, as well as a gamma curve.
|
||||
|
||||
### Configuration
|
||||
In order to use the PWM actuator, you need to configure which pins the PWM output channels will be connected to.
|
||||
There are a couple of `#define`s that achieve this.
|
||||
They can be defined in the `build_flags` section of the env, or by directly editing `config_PWM.h`.
|
||||
|
||||
```c
|
||||
#define PWM_CHANNEL_NAMES {"r", "g", "b", "w0", "w1"}
|
||||
#define PWM_CHANNEL_PINS { 25, 33, 32, 23, 22}
|
||||
```
|
||||
|
||||
`PWM_CHANNEL_NAMES` lists the names that you would like to assign to each channel, and determines the number of channels.
|
||||
`PWM_CHANNEL_PINS` lists the corresponding output pins that the channels will be connected to.
|
||||
The number of entries in `PWM_CHANNEL_PINS` must exactly match the number of entries in `PWM_CHANNEL_NAMES`.
|
||||
|
||||
### Usage
|
||||
|
||||
#### Set
|
||||
`mosquitto_pub -t home/OpenMQTTGateway_MEGA/commands/MQTTtoPWM/set -m '{"r":0.5,"g":0.2,"b":1,"fade":10.0}'`
|
||||
|
||||
This example sets new values for the channels named `r`, `g`, and `b`.
|
||||
These channels will transition from their current values to the new values over 10s.
|
||||
|
||||
#### Calibrate
|
||||
Calibration allows that min and max levels to be configured for each channel, so that the full 0-1 range of values
|
||||
that can be specified with the `set` command actually do things.
|
||||
|
||||
`mosquitto_pub -t home/OpenMQTTGateway_MEGA/commands/MQTTtoPWM/calibrate -m '{"min-r":0.01,"max-r":1.0,"gamma-r":2.5}'`
|
||||
|
||||
This example calibrates the channel named `r`.
|
||||
After this calibration, if you set the `r` channel to 0.0, it will be remapped to 0.01 internally.
|
||||
Also, the gamma curve for this channel will be set to 2.5.
|
||||
This means that input values are raised to the power 2.5 internally.
|
||||
This can be used to improve the linearity of inputs.
|
||||
+4
-3
@@ -225,10 +225,11 @@ uint8_t wifiProtocol = 0; // default mode, automatic selection
|
||||
//#define ZsensorGPIOKeyCode "GPIOKeyCode" //ESP8266, Arduino, ESP32
|
||||
//#define ZsensorGPIOInput "GPIOInput" //ESP8266, Arduino, ESP32
|
||||
//#define ZmqttDiscovery "HADiscovery"//ESP8266, Arduino, ESP32, Sonoff RF Bridge
|
||||
//#define ZactuatorFASTLED "FASTLED" //ESP8266, Arduino, ESP32, Sonoff RF Bridge
|
||||
//#define ZactuatorFASTLED "FASTLED" //ESP8266, Arduino, ESP32, Sonoff RF Bridge
|
||||
//#define ZboardM5STICKC "M5StickC"
|
||||
//#define ZboardM5STACK "ZboardM5STACK"
|
||||
//#define ZradioCC1101 "CC1101" //ESP8266, ESP32
|
||||
//#define ZboardM5STACK "ZboardM5STACK"
|
||||
//#define ZradioCC1101 "CC1101" //ESP8266, ESP32
|
||||
//#define ZactuatorPWM "PWM" //ESP8266, ESP32
|
||||
|
||||
/*-------------DEFINE YOUR MQTT ADVANCED PARAMETERS BELOW----------------*/
|
||||
#ifndef version_Topic
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
OpenMQTTGateway - ESP32, ESP8266 or Arduino program for home automation
|
||||
|
||||
This actuator enables control over the PWM outputs of microcontrollers.
|
||||
|
||||
It uses the highest resolution duty cycles available, to try to
|
||||
give good control over LEDs, particularly at low brightness settings.
|
||||
|
||||
It supports different gamma curves to try to give good perceptually
|
||||
linear results when used to control LEDs, and it supports calibration
|
||||
of the min and max levels for each channel.
|
||||
|
||||
It supports as many channels as the micro-controller can output PWM
|
||||
signals for. Each channel can be named in the config header.
|
||||
|
||||
Supported MQTT topics...
|
||||
|
||||
".../commands/MQTTtoPWM/set" : Set the state of one or more channels.
|
||||
All values are floating point.
|
||||
{
|
||||
"r" : 0.0 - 1.0,
|
||||
"g" : 0.0 - 1.0,
|
||||
"b" : 0.0 - 1.0,
|
||||
"w0" : 0.0 - 1.0,
|
||||
"w1" : 0.0 - 1.0,
|
||||
|
||||
"fade" : <fade time in seconds>
|
||||
}
|
||||
|
||||
".../commands/MQTTtoPWM/calibrate" : Set calibration data
|
||||
All values support floating point for greater precision.
|
||||
It can be convenient to use the 'retain' feature of MQTT to
|
||||
store calibration data.
|
||||
Gamma is a power value that is used help make the intensity
|
||||
curves respond in a more perceptually linear way.
|
||||
If you would like the values to remain linear, set the gamma
|
||||
values to 1.0
|
||||
Min and max set the levels that 0 and 1 will correspond to.
|
||||
Be aware the the min and max values are in the current gamma space
|
||||
before the conversion to linear - so if you change the gamma level
|
||||
then you will probably need to tune the min and max values again
|
||||
{
|
||||
"gamma-r" : 0.5 - 4.0,
|
||||
"min-r" : 0.0 - 1.0,
|
||||
"max-r" : 0.0 - 1.0,
|
||||
|
||||
"gamma-g" : 0.5 - 4.0,
|
||||
etc...
|
||||
}
|
||||
|
||||
Copyright: (c)
|
||||
|
||||
This file is part of OpenMQTTGateway.
|
||||
|
||||
OpenMQTTGateway is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
OpenMQTTGateway is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "User_config.h"
|
||||
|
||||
#ifdef ZactuatorPWM
|
||||
|
||||
# include "config_PWM.h"
|
||||
|
||||
static long previousUpdateTime = 0; // milliseconds
|
||||
static long currentUpdateTime = 0; // milliseconds
|
||||
|
||||
static const char* channelJsonKeys[] = PWM_CHANNEL_NAMES;
|
||||
static const int channelPins[] = PWM_CHANNEL_PINS;
|
||||
static const int kNumChannels = sizeof(channelPins) / sizeof(int);
|
||||
|
||||
// These are all in a perceptually linear colour space, but scaled 0-1
|
||||
static float currentValues[kNumChannels] = {};
|
||||
static float fadeStartValues[kNumChannels] = {};
|
||||
static float targetValues[kNumChannels] = {};
|
||||
|
||||
static long fadeStartUpdateTime[kNumChannels] = {}; // milliseconds
|
||||
static long fadeEndUpdateTime[kNumChannels] = {}; // milliseconds
|
||||
static bool fadeIsComplete = false;
|
||||
|
||||
// Calibration data (initialised during setupPWM)
|
||||
static float calibrationMinLinear[kNumChannels];
|
||||
static float calibrationMaxLinear[kNumChannels];
|
||||
static float calibrationGamma[kNumChannels];
|
||||
|
||||
# if defined(ESP32)
|
||||
// We use the highest resolution duty cycle mode (16-bit) for ESP32, which
|
||||
// gives us good control LEDs at the very dim end of the range.
|
||||
static const int kNumDutyCycleBits = 16;
|
||||
# elif defined(ESP8266)
|
||||
// ESP8266 has a 10-bit duty cycle, which is better than 8 bits, but still
|
||||
// not good enough to give really good dim control over LEDs.
|
||||
static const int kNumDutyCycleBits = 10;
|
||||
# else
|
||||
// Assume Arduino with 8-bit duty cycle.
|
||||
// With an 8-bit duty cycle, it's difficult to get good control over
|
||||
// LEDs at the dark end of the range.
|
||||
static const int kNumDutyCycleBits = 8;
|
||||
# endif
|
||||
|
||||
static const float kUNormToDutyCycle = (float)((1 << kNumDutyCycleBits) - 1);
|
||||
|
||||
void setupPWM() {
|
||||
// Setup the PWM channels at the highest frequency we can for full 16-bit
|
||||
// duty cycle control. These channels will be assigned to the
|
||||
// associated output pins.
|
||||
|
||||
// PWM outputs vary the light intensity linearly when used to control
|
||||
// LEDs. But our eyes don't perceive actual linear changes as linear.
|
||||
// This manifests as a problem when trying to have fine control
|
||||
// over LEDs at very low levels.
|
||||
// Using an 8-bit duty cycle for example only allows for 256 different
|
||||
// linear levels of brightness. Perceptually, the difference in
|
||||
// brightness between levels 1 and 2 is very large - so to get fine
|
||||
// control at dark levels, it's important that we use as high
|
||||
// resolution PWM as we can.
|
||||
for (int i = 0; i < kNumChannels; ++i) {
|
||||
# if defined(ESP32)
|
||||
// Configure the pin for PWM output.
|
||||
// I think this is the fastest frequency that allows for a 16-bit
|
||||
// duty cycle on an ESP32
|
||||
ledcSetup(i, 625.0, kNumDutyCycleBits);
|
||||
ledcAttachPin(channelPins[i], i);
|
||||
# endif
|
||||
calibrationMinLinear[i] = 0.f;
|
||||
calibrationMaxLinear[i] = 1.f;
|
||||
calibrationGamma[i] = PWM_DEFAULT_GAMMA;
|
||||
}
|
||||
|
||||
Log.trace(F("ZactuatorPWM setup done " CR));
|
||||
}
|
||||
|
||||
// This applies a power curve to the input to try to make the inputs
|
||||
// be 'perceptually linear', as opposed to actually linear.
|
||||
static float perceptualToLinear(float perceptual, int channelIdx) {
|
||||
return pow(perceptual, calibrationGamma[channelIdx]);
|
||||
}
|
||||
|
||||
// If we're currently fading between states, then update those states
|
||||
void PWMLoop() {
|
||||
previousUpdateTime = currentUpdateTime;
|
||||
currentUpdateTime = millis();
|
||||
|
||||
if (fadeIsComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
fadeIsComplete = true; //< If any channel isn't finished, we'll set this to false
|
||||
for (int i = 0; i < kNumChannels; ++i) {
|
||||
// Calculate our lerp value through the current fade
|
||||
long totalFadeDuration = fadeEndUpdateTime[i] - fadeStartUpdateTime[i];
|
||||
float fadeLerpValue = 1.f;
|
||||
if (totalFadeDuration > 0) {
|
||||
fadeLerpValue = (float)(currentUpdateTime - fadeStartUpdateTime[i]) / (float)totalFadeDuration;
|
||||
}
|
||||
if (fadeLerpValue >= 1.f) {
|
||||
currentValues[i] = targetValues[i];
|
||||
} else {
|
||||
currentValues[i] = ((targetValues[i] - fadeStartValues[i]) * fadeLerpValue) + fadeStartValues[i];
|
||||
fadeIsComplete = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now convert these perceptually linear values into actually linear values
|
||||
// and set the appropriate duty cycle for the outputs.
|
||||
for (int i = 0; i < kNumChannels; ++i) {
|
||||
float linear = perceptualToLinear(currentValues[i], i);
|
||||
|
||||
// We always treat zero as zero so that it's truly off, regardless of the calibration data.
|
||||
if (linear > 0.f) {
|
||||
// Remap according to the calibration
|
||||
linear = (linear * (calibrationMaxLinear[i] - calibrationMinLinear[i])) + calibrationMinLinear[i];
|
||||
}
|
||||
|
||||
long dutyCycle = (long)(linear * kUNormToDutyCycle);
|
||||
# if defined(ESP32)
|
||||
ledcWrite(i, dutyCycle);
|
||||
# else
|
||||
analogWrite(channelPins[i], dutyCycle);
|
||||
# endif
|
||||
//Log.notice(F("Setting channel %d : %d" CR),i,dutyCycle);
|
||||
}
|
||||
}
|
||||
|
||||
boolean PWMtoMQTT() {
|
||||
return false;
|
||||
}
|
||||
|
||||
# ifdef jsonReceiving
|
||||
void MQTTtoPWM(char* topicOri, JsonObject& jsonData) {
|
||||
if (cmpToMainTopic(topicOri, subjectMQTTtoPWMset)) {
|
||||
Log.trace(F("MQTTtoPWM JSON analysis" CR));
|
||||
// Parse the target value for each channel
|
||||
int modifiedChannelBits = 0;
|
||||
for (int i = 0; i < kNumChannels; ++i) {
|
||||
fadeStartValues[i] = currentValues[i];
|
||||
JsonVariant value = jsonData[channelJsonKeys[i]];
|
||||
if (value.success()) {
|
||||
float targetValue = value.as<float>();
|
||||
targetValue = min(targetValue, 1.f);
|
||||
targetValue = max(targetValue, 0.f);
|
||||
targetValues[i] = targetValue;
|
||||
|
||||
// Initially configure as an instantaneous change....
|
||||
fadeStartUpdateTime[i] = currentUpdateTime;
|
||||
fadeEndUpdateTime[i] = currentUpdateTime;
|
||||
|
||||
modifiedChannelBits |= (1 << i);
|
||||
}
|
||||
}
|
||||
// ...unless there is a "fade" value in the JSON
|
||||
JsonVariant fade = jsonData["fade"];
|
||||
if (fade.success()) {
|
||||
// "fade" json value is in seconds. Convert to milliseconds
|
||||
long endUpdateTime = currentUpdateTime + (long)(fade.as<float>() * (1000.f));
|
||||
// Set the fadeEndUpdateTime for the channels that were modified in the JSON
|
||||
for (int i = 0; i < kNumChannels; ++i) {
|
||||
if (modifiedChannelBits & (1 << i)) {
|
||||
fadeEndUpdateTime[i] = endUpdateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
fadeIsComplete = false; // The values will start to change during PWMLoop
|
||||
} else if (cmpToMainTopic(topicOri, subjectMQTTtoPWMcalibrate)) {
|
||||
// Read the optional calibration data for each channel
|
||||
for (int i = 0; i < kNumChannels; ++i) {
|
||||
char key[64];
|
||||
snprintf(key, sizeof(key), "gamma-%s", channelJsonKeys[i]);
|
||||
JsonVariant value = jsonData[key];
|
||||
if (value.success()) {
|
||||
float gamma = value.as<float>();
|
||||
// Sanity check
|
||||
gamma = min(gamma, 4.f);
|
||||
gamma = max(gamma, 0.5f);
|
||||
calibrationGamma[i] = gamma;
|
||||
}
|
||||
snprintf(key, sizeof(key), "min-%s", channelJsonKeys[i]);
|
||||
value = jsonData[key];
|
||||
if (value.success()) {
|
||||
calibrationMinLinear[i] = perceptualToLinear(value.as<float>(), i);
|
||||
}
|
||||
snprintf(key, sizeof(key), "max-%s", channelJsonKeys[i]);
|
||||
value = jsonData[key];
|
||||
if (value.success()) {
|
||||
calibrationMaxLinear[i] = perceptualToLinear(value.as<float>(), i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
OpenMQTTGateway - ESP8266 or Arduino program for home automation
|
||||
|
||||
This files enables you to set your parameters for the PWM actuator
|
||||
|
||||
Copyright: (c)
|
||||
|
||||
This file is part of OpenMQTTGateway.
|
||||
|
||||
OpenMQTTGateway is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
OpenMQTTGateway is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*-------------------PWM topics & parameters----------------------*/
|
||||
|
||||
// PWM MQTT Subjects
|
||||
#define subjectMQTTtoPWM "/commands/MQTTtoPWM"
|
||||
#define subjectMQTTtoPWMset subjectMQTTtoPWM "/set" //set channel(s) with JSON struct {"r":0-1,"g":0-1,"b":0-1,"w0":0-1,"w0":0-1,"fade":<fade time in seconds>}
|
||||
#define subjectMQTTtoPWMcalibrate subjectMQTTtoPWM "/calibrate" //set calibration data JSON struct {"gamma-r":0.5-4.0,"min-r":0-1,"max-r":0-1 etc. }
|
||||
|
||||
// Edit the following, or declare them in the configuration env, to declare
|
||||
// the channel names and corresponding output pins
|
||||
#ifndef PWM_CHANNEL_NAMES
|
||||
# define PWM_CHANNEL_NAMES \
|
||||
{ "r", "g", "b", "w0", "w1" }
|
||||
#endif
|
||||
#ifndef PWM_CHANNEL_PINS
|
||||
# define PWM_CHANNEL_PINS \
|
||||
{ 25, 33, 32, 23, 22 }
|
||||
#endif
|
||||
|
||||
// Gamma defines a power curve that is applied to convert the input values
|
||||
// to the linear PWM outputs.
|
||||
// For LED control, the gamma can be used to make the inputs be more
|
||||
// perceptually linear.
|
||||
// For controlling other things that require strict linear control, set
|
||||
// the gamma to 1 and leave it alone.
|
||||
// This just defines the default gamma to use for all channels.
|
||||
// The gamma for each channel can be modified using the calibrate MQTT topic.
|
||||
#ifndef PWM_DEFAULT_GAMMA
|
||||
# define PWM_DEFAULT_GAMMA 2.2f
|
||||
#endif
|
||||
@@ -125,6 +125,9 @@ unsigned long timer_sys_measures = 0;
|
||||
#ifdef ZactuatorFASTLED
|
||||
# include "config_FASTLED.h"
|
||||
#endif
|
||||
#ifdef ZactuatorPWM
|
||||
# include "config_PWM.h"
|
||||
#endif
|
||||
#if defined(ZboardM5STICKC) || defined(ZboardM5STACK)
|
||||
# include "config_M5.h"
|
||||
#endif
|
||||
@@ -630,6 +633,9 @@ void setup() {
|
||||
#ifdef ZactuatorFASTLED
|
||||
setupFASTLED();
|
||||
#endif
|
||||
#ifdef ZactuatorPWM
|
||||
setupPWM();
|
||||
#endif
|
||||
#ifdef ZsensorDS1820
|
||||
setupZsensorDS1820();
|
||||
#endif
|
||||
@@ -1201,6 +1207,9 @@ void loop() {
|
||||
#endif
|
||||
#ifdef ZactuatorFASTLED
|
||||
FASTLEDLoop();
|
||||
#endif
|
||||
#ifdef ZactuatorPWM
|
||||
PWMLoop();
|
||||
#endif
|
||||
} else {
|
||||
connectMQTT();
|
||||
@@ -1331,6 +1340,9 @@ void stateMeasures() {
|
||||
# ifdef ZactuatorFASTLED
|
||||
modules = modules + ZactuatorFASTLED;
|
||||
# endif
|
||||
# ifdef ZactuatorPWM
|
||||
modules = modules + ZactuatorPWM;
|
||||
# endif
|
||||
# ifdef ZboardM5STACK
|
||||
M5.Power.begin();
|
||||
SYSdata["m5-batt-level"] = (int8_t)M5.Power.getBatteryLevel();
|
||||
@@ -1456,6 +1468,9 @@ void receivingMQTT(char* topicOri, char* datacallback) {
|
||||
# ifdef ZactuatorFASTLED
|
||||
MQTTtoFASTLED(topicOri, jsondata);
|
||||
# endif
|
||||
# ifdef ZactuatorPWM
|
||||
MQTTtoPWM(topicOri, jsondata);
|
||||
# endif
|
||||
# if defined(ZboardM5STICKC) || defined(ZboardM5STACK)
|
||||
MQTTtoM5(topicOri, jsondata);
|
||||
# endif
|
||||
|
||||
@@ -202,6 +202,7 @@ build_flags =
|
||||
'-DZgatewayBT="BT"'
|
||||
'-DZactuatorONOFF="ONOFF"'
|
||||
'-DZactuatorFASTLED="FASTLED"'
|
||||
'-DZactuatorPWM="PWM"'
|
||||
'-DZsensorINA226="INA226"'
|
||||
'-DZsensorHCSR501="HCSR501"'
|
||||
'-DZsensorADC="ADC"'
|
||||
@@ -418,6 +419,7 @@ build_flags =
|
||||
'-DZgatewayPilight="Pilight"'
|
||||
'-DZactuatorONOFF="ONOFF"'
|
||||
'-DZactuatorFASTLED="FASTLED"'
|
||||
;'-DZactuatorPWM="PWM"'
|
||||
'-DZsensorINA226="INA226"'
|
||||
'-DZsensorHCSR501="HCSR501"'
|
||||
'-DZsensorHCSR04="HCSR04"'
|
||||
@@ -693,6 +695,7 @@ build_flags =
|
||||
'-DZgatewayBT="BT"'
|
||||
'-DZactuatorONOFF="ONOFF"'
|
||||
'-DZactuatorFASTLED="FASTLED"'
|
||||
'-DZactuatorPWM="PWM"'
|
||||
'-DZsensorINA226="INA226"'
|
||||
'-DZsensorHCSR501="HCSR501"'
|
||||
'-DZsensorADC="ADC"'
|
||||
|
||||
Reference in new issue
Block a user