Torc  0.1
torctransitioncontrol.cpp
Go to the documentation of this file.
1 /* Class TorcTransitionControl
2 *
3 * Copyright (C) Mark Kendall 2015-18
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
19 
20 // Qt
21 #include <QVariant>
22 
23 // Torc
24 #include "torclogging.h"
25 #include "torclocalcontext.h"
26 #include "torctimercontrol.h"
27 #include "torctransitioncontrol.h"
28 
29 QEasingCurve::Type TorcTransitionControl::EasingCurveFromString(const QString &Curve)
30 {
31  QString curve = Curve.trimmed().toUpper();
32 
33  // may as well do them all:)
34  if ("LINEAR" == curve) return QEasingCurve::Linear;
35  if ("INQUAD" == curve) return QEasingCurve::InQuad;
36  if ("OUTQUAD" == curve) return QEasingCurve::OutQuad;
37  if ("INOUTQUAD" == curve) return QEasingCurve::InOutQuad;
38  if ("OUTINQUAD" == curve) return QEasingCurve::OutInQuad;
39  if ("INCUBIC" == curve) return QEasingCurve::InCubic;
40  if ("OUTCUBIC" == curve) return QEasingCurve::OutCubic;
41  if ("INOUTCUBIC" == curve) return QEasingCurve::InOutCubic;
42  if ("OUTINCUBIC" == curve) return QEasingCurve::OutInCubic;
43  if ("INQUART" == curve) return QEasingCurve::InQuart;
44  if ("OUTQUART" == curve) return QEasingCurve::OutQuart;
45  if ("INOUTQUART" == curve) return QEasingCurve::InOutQuart;
46  if ("OUTINQUART" == curve) return QEasingCurve::OutInQuart;
47  if ("INQUINT" == curve) return QEasingCurve::InQuint;
48  if ("OUTQUINT" == curve) return QEasingCurve::OutQuint;
49  if ("INOUTQUINT" == curve) return QEasingCurve::InOutQuint;
50  if ("OUTINQUINT" == curve) return QEasingCurve::OutInQuint;
51  if ("INSINE" == curve) return QEasingCurve::InSine;
52  if ("OUTSINE" == curve) return QEasingCurve::OutSine;
53  if ("INOUTSINE" == curve) return QEasingCurve::InOutSine;
54  if ("OUTINSINE" == curve) return QEasingCurve::OutInSine;
55  if ("INEXPO" == curve) return QEasingCurve::InExpo;
56  if ("OUTEXPO" == curve) return QEasingCurve::OutExpo;
57  if ("INOUTEXPO" == curve) return QEasingCurve::InOutExpo;
58  if ("OUTINEXPO" == curve) return QEasingCurve::OutInExpo;
59  if ("INCIRC" == curve) return QEasingCurve::InCirc;
60  if ("OUTCIRC" == curve) return QEasingCurve::OutCirc;
61  if ("INOUTCIRC" == curve) return QEasingCurve::InOutCirc;
62  if ("OUTINCIRC" == curve) return QEasingCurve::OutInCirc;
63  if ("INELASTIC" == curve) return QEasingCurve::InElastic;
64  if ("OUTELASTIC" == curve) return QEasingCurve::OutElastic;
65  if ("INOUTELASTIC" == curve) return QEasingCurve::InOutElastic;
66  if ("OUTINELASTIC" == curve) return QEasingCurve::OutInElastic;
67  if ("INBACK" == curve) return QEasingCurve::InBack;
68  if ("OUTBACK" == curve) return QEasingCurve::OutBack;
69  if ("INOUTBACK" == curve) return QEasingCurve::InOutBack;
70  if ("OUTINBACK" == curve) return QEasingCurve::OutInBack;
71  if ("INBOUNCE" == curve) return QEasingCurve::InBounce;
72  if ("OUTBOUNCE" == curve) return QEasingCurve::OutBounce;
73  if ("INOUTBOUNCE" == curve) return QEasingCurve::InOutBounce;
74  if ("OUTINBOUNCE" == curve) return QEasingCurve::OutInBounce;
75  if ("LINEARLED" == curve) return QEasingCurve::Custom;
76 
77  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Failed to parse transition type from '%1'").arg(Curve));
78  return QEasingCurve::TCBSpline; // invalid
79 }
80 
81 QString TorcTransitionControl::StringFromEasingCurve(QEasingCurve::Type Type)
82 {
83  switch (Type)
84  {
85  case QEasingCurve::Linear: return tr("Linear");
86  case QEasingCurve::InQuad: return tr("InQuad");
87  case QEasingCurve::OutQuad: return tr("OutQuad");
88  case QEasingCurve::InOutQuad: return tr("InOutQuad");
89  case QEasingCurve::OutInQuad: return tr("OutInQuad");
90  case QEasingCurve::InCubic: return tr("InCubic");
91  case QEasingCurve::OutCubic: return tr("OutCubic");
92  case QEasingCurve::InOutCubic: return tr("InOutCubic");
93  case QEasingCurve::OutInCubic: return tr("OutInCubic");
94  case QEasingCurve::InQuart: return tr("InQuart");
95  case QEasingCurve::OutQuart: return tr("OutQuart");
96  case QEasingCurve::InOutQuart: return tr("InOutQuart");
97  case QEasingCurve::OutInQuart: return tr("OutInQuart");
98  case QEasingCurve::InQuint: return tr("InQuint");
99  case QEasingCurve::OutQuint: return tr("OutQuint");
100  case QEasingCurve::InOutQuint: return tr("InOutQuint");
101  case QEasingCurve::OutInQuint: return tr("OutInQuint");
102  case QEasingCurve::InSine: return tr("InSine");
103  case QEasingCurve::OutSine: return tr("OutSine");
104  case QEasingCurve::InOutSine: return tr("InOutSine");
105  case QEasingCurve::OutInSine: return tr("OutInSine");
106  case QEasingCurve::InExpo: return tr("InExpo");
107  case QEasingCurve::OutExpo: return tr("OutExpo");
108  case QEasingCurve::InOutExpo: return tr("InOutExpo");
109  case QEasingCurve::OutInExpo: return tr("OutInExpo");
110  case QEasingCurve::InCirc: return tr("InCirc");
111  case QEasingCurve::OutCirc: return tr("OutCirc");
112  case QEasingCurve::InOutCirc: return tr("InOutCirc");
113  case QEasingCurve::OutInCirc: return tr("OutInCirc");
114  case QEasingCurve::InElastic: return tr("InElastic");
115  case QEasingCurve::OutElastic: return tr("OutElastic");
116  case QEasingCurve::InOutElastic: return tr("InOutElastic");
117  case QEasingCurve::OutInElastic: return tr("OutInElastic");
118  case QEasingCurve::InBack: return tr("InBack");
119  case QEasingCurve::OutBack: return tr("OutBack");
120  case QEasingCurve::InOutBack: return tr("InOutBack");
121  case QEasingCurve::OutInBack: return tr("OutInBack");
122  case QEasingCurve::InBounce: return tr("InBounce");
123  case QEasingCurve::OutBounce: return tr("OutBounce");
124  case QEasingCurve::InOutBounce: return tr("InOutBounce");
125  case QEasingCurve::OutInBounce: return tr("OutInBounce");
126  case QEasingCurve::Custom: return tr("LinearLED");
127  default:
128  break;
129  }
130 
131  return tr("Unknown");
132 }
133 
144 TorcTransitionControl::TorcTransitionControl(const QString &Type, const QVariantMap &Details)
145  : TorcControl(TorcControl::Transition, Details),
146  m_duration(0),
147  m_type(QEasingCurve::Linear),
148  m_animation(this, "animationValue"),
149  animationValue(0),
150  m_firstTrigger(true),
151  m_transitionValue(0)
152 {
153  // determine curve
154  m_type = EasingCurveFromString(Type);
155  if (m_type == QEasingCurve::TCBSpline /*invalid*/)
156  {
157  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Unknown transition type '%1' for device '%2'").arg(Type, uniqueId));
158  return;
159  }
160 
161  // check duration
162  if (!Details.contains(QStringLiteral("duration")))
163  {
164  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Transition '%1' does not specify duration").arg(uniqueId));
165  return;
166  }
167 
168  int days, hours, minutes, seconds;
169  QString durations = Details.value(QStringLiteral("duration")).toString();
170  quint64 duration;
171  if (!TorcControl::ParseTimeString(durations, days, hours, minutes, seconds, duration))
172  {
173  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to parse transition duration from '%1'").arg(durations));
174  return;
175  }
176 
177  // no point in having a zero length transition
178  if (duration < 1)
179  {
180  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Transition duration is invalid ('%1')").arg(duration));
181  return;
182  }
183  m_duration = duration;
184 
185  // a transition can have a default value (0 or 1) to force the start state properly
186  if (Details.contains(QStringLiteral("default")))
187  {
188  bool ok = false;
189  int defaultvalue = Details.value(QStringLiteral("default")).toInt(&ok);
190  if (ok)
191  {
192  if (defaultvalue == 0 || defaultvalue == 1)
193  {
194  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Setting default for '%1' to '%2'").arg(uniqueId).arg(defaultvalue));
195  defaultValue = defaultvalue;
196  value = defaultvalue; // NB safe to set during constructor
197  }
198  else
199  {
200  ok = false;
201  }
202  }
203 
204  if (!ok)
205  {
206  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to parse default for transition '%1'").arg(uniqueId));
207  return;
208  }
209  }
210 
211  // so far so good
212  m_parsed = true;
213 
214  gLocalContext->AddObserver(this);
215 }
216 
217 TorcTransitionControl::~TorcTransitionControl()
218 {
220 }
221 
222 TorcControl::Type TorcTransitionControl::GetType(void) const
223 {
225 }
226 
227 QStringList TorcTransitionControl::GetDescription(void)
228 {
229  static const quint64 secondsinday = 60 * 60 * 24;
230  int daysduration = m_duration / secondsinday;
231  quint64 timeduration = m_duration % secondsinday;
232 
233  QStringList result;
234  result.append(tr("%1 transition").arg(StringFromEasingCurve(m_type)));
235  result.append(tr("Duration %1").arg(TorcControl::DurationToString(daysduration, timeduration)));
236  return result;
237 }
238 
239 bool TorcTransitionControl::event(QEvent *Event)
240 {
241  if (Event && Event->type() == TorcEvent::TorcEventType)
242  {
243  TorcEvent *event = static_cast<TorcEvent*>(Event);
244  if (event && (event->GetEvent() == Torc::SystemTimeChanged))
245  {
246  // we don't know in which order this event is received, so we don't know
247  // whether any timer inputs have been reset. So wait a short period.
248  QTimer::singleShot(10, this, &TorcTransitionControl::Restart);
249  return true;
250  }
251  }
252 
253  return TorcControl::event(Event);
254 }
255 
257 {
258  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Transition %1 restarting").arg(uniqueId));
259  m_firstTrigger = true;
260  CalculateOutput();
261 }
262 
263 bool TorcTransitionControl::Validate(void)
264 {
265  QMutexLocker locker(&lock);
266 
267  // don't repeat validation
268  if (m_validated)
269  return true;
270 
271  // common checks
272  if (!TorcControl::Validate())
273  return false;
274 
275  // need one input and one input only
276  if (m_inputs.size() != 1)
277  {
278  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Transition device '%1' needs exactly one input").arg(uniqueId));
279  return false;
280  }
281 
282  // need at least one output
283  if (m_outputs.isEmpty())
284  {
285  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Transition device '%1' needs at least one output").arg(uniqueId));
286  return false;
287  }
288 
289  // if we get this far, we can finish the device
290  if (!Finish())
291  return false;
292 
293  // setup the animation now
294  QEasingCurve easingcurve;
295  if (m_type == QEasingCurve::Custom)
296  easingcurve.setCustomType(TorcTransitionControl::LinearLEDFunction);
297  else
298  easingcurve.setType(m_type);
299  m_animation.setEasingCurve(easingcurve);
300  m_animation.setStartValue(0);
301  m_animation.setEndValue(1);
302  m_animation.setDuration(m_duration * 1000);
303 
304  // debug
305  LOG(VB_GENERAL, LOG_DEBUG, QStringLiteral("%1: %2").arg(uniqueId, GetDescription().join(',')));
306 
307  return true;
308 }
309 
317 qreal TorcTransitionControl::LinearLEDFunction(qreal progress)
318 {
319  if (progress <= 0.08)
320  return progress / 9.033;
321  qreal val = (progress + 0.16) / 1.16;
322  return val * val * val;
323 }
324 
337 void TorcTransitionControl::CalculateOutput(void)
338 {
339  QMutexLocker locker(&lock);
340 
341  // sanity check
342  if (m_inputs.size() != 1)
343  return;
344 
345  quint64 timesincelasttransition = 0;
346 
347  // If the input is a timer, at startup we need to check the status of the timer and the time
348  // elapsed since its last transition. We can then force the animation output.
349  // This handles, for example, a light dimmer that is configured for a 1 hour sunrise/sunset.
350  // If we need to turn the system off during the day, we don't want the lights to take another hour
351  // to turn back on - but to move to the value we would expect for the time of day. This could
352  // perhaps be configurable (some people might want them to take the hour) but sudden transitions
353  // could also be 'smoothed' with a seperate transition.
354 
355  double newvalue = m_inputValues.constBegin().value();
356  LOG(VB_GENERAL, LOG_DEBUG, QStringLiteral("Transition value: %1").arg(newvalue));
357 
358  if (m_firstTrigger)
359  {
360  m_firstTrigger = false;
361  m_transitionValue = newvalue;
362 
363  TorcTimerControl *timerinput = qobject_cast<TorcTimerControl*>(m_inputs.firstKey());
364  if (timerinput)
365  {
366  timesincelasttransition = timerinput->TimeSinceLastTransition() / 1000;
367 
368  if (timesincelasttransition > m_duration)
369  {
370  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Transition '%1' is initially inactive (value '%2')").arg(uniqueId).arg(newvalue));
371  SetValue(newvalue);
372  return;
373  }
374 
375  // if we are part way through the transition, the animation will expect the value to have started
376  // from the previous transition value !:)
377  SetValue(newvalue > 0 ? 0 : 1);
378  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Forcing transition '%1' to %2% complete (%3)").arg(uniqueId)
379  .arg(((double)timesincelasttransition / (double)m_duration) * 100.0).arg(newvalue ? QStringLiteral("rising") : QStringLiteral("falling")));
380 
381  if (newvalue < 1) // time can run backwards :)
382  timesincelasttransition = m_duration - timesincelasttransition;
383  }
384  else // not a timer input - just a regular input (ideally zero or one) - don't start the timer without reason
385  {
386  if (qFuzzyCompare(GetValue() + 1.0, m_transitionValue + 1.0))
387  {
388  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Transition '%1' is initially inactive (value '%2')").arg(uniqueId).arg(newvalue));
389  return;
390  }
391  }
392  }
393  else
394  {
395  if (qFuzzyCompare(newvalue + 1.0, m_transitionValue + 1.0))
396  return;
397  }
398 
399  m_transitionValue = newvalue;
400 
401  // We assume the specified easing curve is for a rising (0-1) transition. We then run it in
402  // reverse for the 'mirrored' falling operation.
403  // NB if the animation is still running, it will reverse from its current position - so there
404  // will be no glitches/jumps/interruptions.
405  m_animation.setDirection(newvalue > 0 ? QAbstractAnimation::Forward : QAbstractAnimation::Backward);
406  m_animation.start();
407 
408  // this has to come after start
409  if (timesincelasttransition > 0)
410  m_animation.setCurrentTime(timesincelasttransition * 1000);
411 }
412 
415 {
416  QMutexLocker locker(&lock);
417  animationValue = Value;
418  SetValue(Value);
419 }
420 
422 {
423  QMutexLocker locker(&lock);
424  return animationValue;
425 }
static bool ParseTimeString(const QString &Time, int &Days, int &Hours, int &Minutes, int &Seconds, quint64 &DurationInSeconds)
Parse a Torc time string into days, hours, minutes and, if present, seconds.
Definition: torccontrol.cpp:36
TorcLocalContext * gLocalContext
virtual bool Validate(void)
QString uniqueId
Definition: torcdevice.h:63
QMap< QObject *, QString > m_inputs
Definition: torccontrol.h:71
bool m_validated
Definition: torccontrol.h:68
QMap< QObject *, double > m_inputValues
Definition: torccontrol.h:73
bool m_parsed
Definition: torccontrol.h:67
double defaultValue
Definition: torcdevice.h:61
VERBOSE_PREAMBLE true
quint64 TimeSinceLastTransition(void)
void SetValue(double Value) override
QMutex lock
Definition: torcdevice.h:66
static Type TorcEventType
Register TorcEventType with QEvent.
Definition: torcevent.h:19
static QString DurationToString(int Days, quint64 Duration)
double value
Definition: torcdevice.h:60
void SetAnimationValue(double Value)
Our main output, value, is read only. So the animation operates on a proxy, animationValue.
bool Finish(void)
Finish setup of the control.
A general purpose event object.
Definition: torcevent.h:9
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: torclogging.h:20
QStringList GetDescription(void) override
void AddObserver(QObject *Observer)
brief Register the given object to receive events.
void RemoveObserver(QObject *Observer)
brief Deregister the given object.
bool event(QEvent *Event) override
QMap< QObject *, QString > m_outputs
Definition: torccontrol.h:72
double GetValue(void)
Definition: torcdevice.cpp:142
bool event(QEvent *Event) override