mirror of
https://github.com/1technophile/OpenMQTTGateway.git
synced 2026-02-25 03:01:44 +01:00
By building only the log level required into LOG_LEVEL Co-authored-by: Florian <1technophile@users.noreply.github.com>
261 lines
9.4 KiB
C++
261 lines
9.4 KiB
C++
/*
|
|
Theengs OpenMQTTGateway - We Unite Sensors in One Open-Source Interface
|
|
|
|
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 "TheengsCommon.h"
|
|
# 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
|
|
ledcAttachChannel(channelPins[i], 625.0, kNumDutyCycleBits, i);
|
|
# endif
|
|
calibrationMinLinear[i] = 0.f;
|
|
calibrationMaxLinear[i] = 1.f;
|
|
calibrationGamma[i] = PWM_DEFAULT_GAMMA;
|
|
}
|
|
|
|
THEENGS_LOG_TRACE(F("actuatorPWM 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
|
|
//THEENGS_LOG_NOTICE(F("Setting channel %d : %d" CR),i,dutyCycle);
|
|
}
|
|
}
|
|
|
|
boolean PWMtoX() {
|
|
return false;
|
|
}
|
|
|
|
# if jsonReceiving
|
|
void XtoPWM(const char* topicOri, JsonObject& jsonData) {
|
|
if (cmpToMainTopic(topicOri, subjectMQTTtoPWMset)) {
|
|
THEENGS_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.isNull()) {
|
|
float targetValue = value.as<float>();
|
|
targetValue = std::min(targetValue, 1.f);
|
|
targetValue = std::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.isNull()) {
|
|
// "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.isNull()) {
|
|
float gamma = value.as<float>();
|
|
// Sanity check
|
|
gamma = std::min(gamma, 4.f);
|
|
gamma = std::max(gamma, 0.5f);
|
|
calibrationGamma[i] = gamma;
|
|
}
|
|
snprintf(key, sizeof(key), "min-%s", channelJsonKeys[i]);
|
|
value = jsonData[key];
|
|
if (!value.isNull()) {
|
|
calibrationMinLinear[i] = perceptualToLinear(value.as<float>(), i);
|
|
}
|
|
snprintf(key, sizeof(key), "max-%s", channelJsonKeys[i]);
|
|
value = jsonData[key];
|
|
if (!value.isNull()) {
|
|
calibrationMaxLinear[i] = perceptualToLinear(value.as<float>(), i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
# endif
|
|
|
|
#endif
|