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:
Oli Wright authored and GitHub committed 2020-09-02 11:17:36 -05:00
1 parent 7b9944d1ca
commit ef39e14497
6 files changed
+374 -3

No files matched your search

+4 -3
View File
@@ -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
+261
View File
@@ -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
+52
View File
@@ -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
+15
View File
@@ -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