diff --git a/docs/use/actuators.md b/docs/use/actuators.md index a73ca82..f6de48c 100644 --- a/docs/use/actuators.md +++ b/docs/use/actuators.md @@ -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. diff --git a/main/User_config.h b/main/User_config.h index c46d199..6a9cfb3 100755 --- a/main/User_config.h +++ b/main/User_config.h @@ -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 diff --git a/main/ZactuatorPWM.ino b/main/ZactuatorPWM.ino new file mode 100644 index 0000000..33a844e --- /dev/null +++ b/main/ZactuatorPWM.ino @@ -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" : + } + + ".../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 . +*/ +#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(); + 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() * (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(); + // 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(), i); + } + snprintf(key, sizeof(key), "max-%s", channelJsonKeys[i]); + value = jsonData[key]; + if (value.success()) { + calibrationMaxLinear[i] = perceptualToLinear(value.as(), i); + } + } + } +} +# endif + +#endif diff --git a/main/config_PWM.h b/main/config_PWM.h new file mode 100644 index 0000000..bf90c1f --- /dev/null +++ b/main/config_PWM.h @@ -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 . +*/ + +/*-------------------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":} +#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 diff --git a/main/main.ino b/main/main.ino index 1a1301d..065f23e 100644 --- a/main/main.ino +++ b/main/main.ino @@ -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 diff --git a/platformio.ini b/platformio.ini index 6c7ad91..25963c5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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"'