1 /**
2  * Copyright (C) Francesco Fusco. All rights reserved.
3  * License: https://github.com/Fushko/gammy#license
4  */
5 
6 #include <QTime>
7 #include <thread>
8 #include "gammactl.h"
9 #include "defs.h"
10 #include "utils.h"
11 #include "cfg.h"
12 #include "mediator.h"
13 
GammaCtl()14 GammaCtl::GammaCtl()
15 {
16 	// If auto brightness is on, start at max brightness
17 	if (cfg["brt_auto"].get<bool>())
18 		cfg["brt_step"] = brt_steps_max;
19 
20 	// If auto temp is on, start at max temp for a smooth transition
21 	if (cfg["temp_auto"].get<bool>())
22 		cfg["temp_step"] = 0;
23 
24 	setGamma(cfg["brt_step"].get<int>(), cfg["temp_step"].get<int>());
25 }
26 
start()27 void GammaCtl::start()
28 {
29 	LOGD << "Starting gamma control";
30 
31 	if (!threads.empty())
32 		return;
33 
34 	threads.emplace_back(std::thread([this] { adjustTemperature(); }));
35 	threads.emplace_back(std::thread([this] { captureScreen(); }));
36 	threads.emplace_back(std::thread([this] { reapplyGamma(); }));
37 }
38 
stop()39 void GammaCtl::stop()
40 {
41 	LOGD << "Stopping gamma control";
42 
43 	if (threads.empty())
44 		return;
45 
46 	quit = true;
47 	notify_all_threads();
48 
49 	for (auto &t : threads)
50 		t.join();
51 
52 	threads.clear();
53 }
54 
notify_temp(bool force)55 void GammaCtl::notify_temp(bool force)
56 {
57 	force_temp_change = force;
58 	temp_cv.notify_one();
59 }
60 
notify_ss()61 void GammaCtl::notify_ss()
62 {
63 	ss_cv.notify_one();
64 }
65 
notify_all_threads()66 void GammaCtl::notify_all_threads()
67 {
68 	temp_cv.notify_one();
69 	ss_cv.notify_one();
70 	reapply_cv.notify_one();
71 }
72 
reapplyGamma()73 void GammaCtl::reapplyGamma()
74 {
75 	using namespace std::this_thread;
76 	using namespace std::chrono;
77 	using namespace std::chrono_literals;
78 
79 	std::mutex mtx;
80 
81 	while (true) {
82 		{
83 			std::unique_lock<std::mutex> lock(mtx);
84 			reapply_cv.wait_until(lock, system_clock::now() + 5s, [&] {
85 				return quit;
86 			});
87 		}
88 
89 		if (quit)
90 			break;
91 
92 		setGamma(cfg["brt_step"], cfg["temp_step"]);
93 	}
94 }
95 
captureScreen()96 void GammaCtl::captureScreen()
97 {
98 	LOGV << "captureScreen() start";
99 
100 	convar      brt_cv;
101 	std::thread brt_thr([&] { adjustBrightness(brt_cv); });
102 	std::mutex  m;
103 
104 	int img_delta = 0;
105 	bool force    = false;
106 
107 	int
108 	prev_img_br = 0,
109 	prev_min    = 0,
110 	prev_max    = 0,
111 	prev_offset = 0;
112 
113 	while (true) {
114 		{
115 			std::unique_lock<std::mutex> lock(m);
116 
117 			ss_cv.wait(lock, [&] {
118 				return cfg["brt_auto"].get<bool>() || quit;
119 			});
120 		}
121 
122 		if (quit)
123 			break;
124 
125 		if (cfg["brt_auto"].get<bool>())
126 			force = true;
127 		else
128 			continue;
129 
130 		while (cfg["brt_auto"].get<bool>() && !quit) {
131 
132 			const int img_br = getScreenBrightness();
133 			img_delta += abs(prev_img_br - img_br);
134 
135 			if (img_delta > cfg["brt_threshold"].get<int>() || force) {
136 
137 				img_delta = 0;
138 				force = false;
139 
140 				{
141 					std::lock_guard lock(brt_mtx);
142 					this->ss_brightness = img_br;
143 					br_needs_change = true;
144 				}
145 
146 				brt_cv.notify_one();
147 			}
148 
149 			if (cfg["brt_min"] != prev_min || cfg["brt_max"] != prev_max || cfg["brt_offset"] != prev_offset)
150 				force = true;
151 
152 			prev_img_br = img_br;
153 			prev_min    = cfg["brt_min"];
154 			prev_max    = cfg["brt_max"];
155 			prev_offset = cfg["brt_offset"];
156 
157 			// On Windows, we sleep in getScreenBrightness()
158 			if constexpr (!windows) {
159 				std::this_thread::sleep_for(std::chrono::milliseconds(cfg["brt_polling_rate"].get<int>()));
160 			}
161 		}
162 	}
163 
164 	{
165 		std::lock_guard<std::mutex> lock (brt_mtx);
166 		br_needs_change = true;
167 	}
168 
169 	brt_cv.notify_one();
170 	brt_thr.join();
171 }
172 
adjustBrightness(convar & brt_cv)173 void GammaCtl::adjustBrightness(convar &brt_cv)
174 {
175 	using namespace std::this_thread;
176 	using namespace std::chrono;
177 
178 	while (true) {
179 		int img_br;
180 
181 		{
182 			std::unique_lock<std::mutex> lock(brt_mtx);
183 
184 			brt_cv.wait(lock, [&] {
185 				return br_needs_change;
186 			});
187 
188 			if (quit)
189 				break;
190 
191 			br_needs_change = false;
192 			img_br = this->ss_brightness;
193 		}
194 
195 		const int cur_step = cfg["brt_step"];
196 		const int tmp = brt_steps_max
197 		                - int(remap(img_br, 0, 255, 0, brt_steps_max))
198 		                + int(remap(cfg["brt_offset"].get<int>(), 0, brt_steps_max, 0, cfg["brt_max"].get<int>()));
199 		const int target_step = std::clamp(tmp, cfg["brt_min"].get<int>(), cfg["brt_max"].get<int>());
200 
201 		if (cur_step == target_step) {
202 			LOGV << "Brt already at target (" << target_step << ')';
203 			continue;
204 		}
205 
206 		double time             = 0;
207 		const int FPS           = cfg["brt_fps"];
208 		const double slice      = 1. / FPS;
209 		const double duration_s = cfg["brt_speed"].get<double>() / 1000;
210 		const int diff          = target_step - cur_step;
211 
212 		while (cfg["brt_step"].get<int>() != target_step) {
213 
214 			if (br_needs_change || !cfg["brt_auto"].get<bool>() || quit)
215 				break;
216 
217 			time += slice;
218 			cfg["brt_step"] = int(std::round(easeOutExpo(time, cur_step, diff, duration_s)));
219 
220 			setGamma(cfg["brt_step"], cfg["temp_step"]);
221 			mediator->notify(this, BRT_CHANGED);
222 			sleep_for(milliseconds(1000 / FPS));
223 		}
224 	}
225 }
226 
227 /**
228  * The temperature is adjusted in two steps.
229  * The first one is for quickly catching up to the proper temperature when:
230  * - time is checked sometime after the start time
231  * - the system wakes up
232  * - temperature settings change
233  */
adjustTemperature()234 void GammaCtl::adjustTemperature()
235 {
236 	using namespace std::this_thread;
237 	using namespace std::chrono;
238 	using namespace std::chrono_literals;
239 
240 	QTime start_time;
241 	QTime end_time;
242 
243 	const auto setTime = [] (QTime &t, const std::string &time_str) {
244 		const auto start_h = time_str.substr(0, 2);
245 		const auto start_m = time_str.substr(3, 2);
246 		t = QTime(std::stoi(start_h), std::stoi(start_m));
247 	};
248 
249 	const auto updateInterval = [&] {
250 		std::string t_start = cfg["temp_sunset"];
251 		const int h = std::stoi(t_start.substr(0, 2));
252 		const int m = std::stoi(t_start.substr(3, 2));
253 
254 		const int adapt_time_s = cfg["temp_speed"].get<double>() * 60;
255 		const QTime adapted_start = QTime(h, m).addSecs(-adapt_time_s);
256 
257 		setTime(start_time, adapted_start.toString().toStdString());
258 		setTime(end_time, cfg["temp_sunrise"]);
259 	};
260 
261 	updateInterval();
262 
263 	bool needs_change = cfg["temp_auto"];
264 
265 	convar     clock_cv;
266 	std::mutex clock_mtx;
267 	std::mutex temp_mtx;
268 
269 	std::thread clock ([&] {
270 		while (true) {
271 			{
272 				std::unique_lock<std::mutex> lk(clock_mtx);
273 				clock_cv.wait_until(lk, system_clock::now() + 60s, [&] {
274 					return quit;
275 				});
276 			}
277 
278 			if (quit)
279 				break;
280 
281 			if (!cfg["temp_auto"])
282 				continue;
283 
284 			{
285 				std::lock_guard<std::mutex> lock(temp_mtx);
286 				needs_change = true; // @TODO: Should be false if the state hasn't changed
287 			}
288 
289 			temp_cv.notify_one();
290 		}
291 	});
292 
293 	bool first_step_done = false;
294 
295 	while (true) {
296 		{
297 			std::unique_lock<std::mutex> lock(temp_mtx);
298 
299 			temp_cv.wait(lock, [&] {
300 				return needs_change || first_step_done || force_temp_change || quit;
301 			});
302 
303 			if (quit)
304 				break;
305 
306 			if (force_temp_change) {
307 				updateInterval();
308 				force_temp_change = false;
309 				first_step_done = false;
310 			}
311 
312 			needs_change = false;
313 		}
314 
315 		if (!cfg["temp_auto"])
316 			continue;
317 
318 
319 		int    target_temp = cfg["temp_low"]; // Temperature target in Kelvin
320 		double duration_s  = 2;               // Seconds it takes to reach it
321 
322 		const double    adapt_time_s = cfg["temp_speed"].get<double>() * 60;
323 		const QDateTime cur_datetime = QDateTime::currentDateTime();
324 		const QTime     cur_time     = cur_datetime.time();
325 
326 		if ((cur_time >= start_time) || (cur_time < end_time)) {
327 
328 			QDateTime start_datetime(cur_datetime.date(), start_time);
329 
330 			/* If we are earlier than both sunset and sunrise times
331 			 * we need to count from yesterday. */
332 			if (cur_time < end_time)
333 				start_datetime = start_datetime.addDays(-1);
334 
335 			int secs_from_start = start_datetime.secsTo(cur_datetime);
336 
337 			LOGV << "secs_from_start: " << secs_from_start << " adapt_time_s: " << adapt_time_s;
338 
339 			if (secs_from_start > adapt_time_s)
340 				secs_from_start = adapt_time_s;
341 
342 			if (!first_step_done) {
343 				target_temp = remap(secs_from_start, 0, adapt_time_s, cfg["temp_high"], cfg["temp_low"]);
344 			} else {
345 				duration_s = adapt_time_s - secs_from_start;
346 				if (duration_s < 2)
347 					duration_s = 2;
348 			}
349 		} else {
350 			target_temp = cfg["temp_high"];
351 		}
352 
353 		LOGV << "Temp duration: " << duration_s / 60 << " min";
354 
355 		int cur_step    = cfg["temp_step"];
356 		int target_step = int(remap(target_temp, temp_k_max, temp_k_min, temp_steps_max, 0));
357 
358 		if (cur_step == target_step) {
359 			first_step_done = false;
360 			continue;
361 		}
362 
363 		double time        = 0;
364 		const int FPS      = cfg["temp_fps"];
365 		const double slice = 1. / FPS;
366 		const int diff     = target_step - cur_step;
367 
368 		while (cfg["temp_step"].get<int>() != target_step) {
369 
370 			if (force_temp_change || !cfg["temp_auto"].get<bool>() || quit)
371 				break;
372 
373 			time += slice;
374 			cfg["temp_step"] = int(easeInOutQuad(time, cur_step, diff, duration_s));
375 
376 			setGamma(cfg["brt_step"], cfg["temp_step"]);
377 			mediator->notify(this, TEMP_CHANGED);
378 			sleep_for(milliseconds(1000 / FPS));
379 		}
380 
381 		first_step_done = true;
382 	}
383 
384 	clock_cv.notify_one();
385 	clock.join();
386 }
387