Files
OpenMQTTGateway/main/actuatorPWM.cpp
Ryan Powell 585df9a420 [SYS] Update arduino core to 3.1.1 + refactor ino to cpp (#2177)
* Update arduino core to 3.1.1

* Fix Blufi build

* Update arduinojson, fix build errors with idf

* Fix narrowing

* fix RF builds

* WIP-Convert ino files to cpp

* Fix pilight build

* Fix Somfy actuator build.

* Update esp32dev-rf partition

* Fix Weatherstation build

* Fix GFSunInverter build

* Fix esp32dev-ir build

* Fix ble-aws build

* Fix eth builds

* Fix m5Stack missing pins_arduino.h

* Fix build errors for M5 stack/tough, others are upstream issues.

* Fix RTL 433 build - remaining errors are from radolib

* Fix nodemcu build

* fix 2g builds

* Fix serial build

* Fix actuator on off builds

* Fix SSD1306 build - remaining errors are from radiolib

* Fix multiple definition of OTAserver_cert

* Fix nodemcu rf2 build

* Fix ADC builds

* Fix sensor builds

* Fix LORA builds

* Fix multi-receiver builds - remaining errors are in radiolib

* Fix fastled builds

* Fix theegns board builds

* Fix broker builds

* Update fastled - old version failed all-test build

* Fix RN8209 builds

* Fix max temp actuator builds

* Fix PWM builds

* Fix C37 sensor builds

* Fix HTU21 builds

* Fix INA266 builds

* Fix undefined variables in mqtt discovery

* Fix webui build

* Fix fastled invalid pin error

* Fix wifi manual setup builds

* Fix onewire/all-test build - bin too big error remaining

* Fix theengs plug build

* Fix rfbridge builds

* Fix blufi builds

* Fix undefined functions in serial

* Fix preprocessor definition checks

* Set IDF log level to erre

* Add delay in loop to prevent watchdog timeout

* Use xTaskCreateUniveral to support single core processors

* Remove old HTTPUpdate files - upsteam fixed.

* Cleanup and move common declarations to header file

* Use custom partiton table to fix builds where bin is too large

* Update M5StickC - fixs esp32-m5stick-c-ble build

* Revert to Arduino core 2.x for builds with incompatible libs

* Remove "Z" from file names and rename omg_common to TheengsCommon

* Fix gateway name when using MAC with new Arduino core

* Update IDF config to reduce loop task stack use

* Update esp-nimble-cpp version, corrects BLE uppercase ID's

* Update wifi manager to fix watchdog timeout error
2025-05-06 19:35:50 -05:00

261 lines
9.3 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;
}
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
//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)) {
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