# -*- python -*- # This example demonstrates how you can make a # multithreaded configurations script. # This file is dual-licensed under GNU General Public License # version 3 and GNU Free Documentation License version 1.3. # Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) # # This program 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. # # This program 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/>. # Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) # # Permission is granted to copy, distribute and/or modify this document # under the terms of the GNU Free Documentation License, Version 1.3 # or any later version published by the Free Software Foundation; # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. # You should have received a copy of the GNU General Public License # along with this software package. If not, see <http://www.gnu.org/licenses/>. import threading # Geographical coodinates. # ("Kristall, vertikal accent i glas och stål" (Crystal, vertical accent # in glass and steal) in this example. A glass obelisk, lit from the inside # with adjustable colours and a default colour of 5600 K, in the middle # of a hyperelliptic roundabout.) latitude, longitude = 59.3326, 18.0652 # Adjust settings by solar elevation. get_dayness = lambda : sun(latitude, longitude) # Colour temperature at high day and high night, respectively. temperature_day, temperature_night = [6500], [3700] # Gamma of the monitors. gamma_red = [1.16, 1.10] gamma_green = [1.15, 1.16] gamma_blue = [1.11, 1.10] # Make colour curves thread local. class threadlocal: def __init__(self, obj): self.default = obj self.tmap = {} def __getitem__(self, i): t = threading.get_ident() if t not in self.tmap: self.tmap[t] = self.default[:] return self.tmap[t][i] def __len__(self): t = threading.get_ident() if t not in self.tmap: self.tmap[t] = self.default[:] return len(self.tmap[t]) def __setitem__(self, i, x): t = threading.get_ident() if t not in self.tmap: self.tmap[t] = self.default[:] self.tmap[t][i] = x def __delitem__(self, i): t = threading.get_ident() if t not in self.tmap: self.tmap[t] = self.default[:] del self.tmap[t][x] r_curve = threadlocal([i / (i_size - 1) for i in range(i_size)]) g_curve = threadlocal([i / (i_size - 1) for i in range(i_size)]) b_curve = threadlocal([i / (i_size - 1) for i in range(i_size)]) ## cmf_10deg uses non-thread safe cache, run once in advance so it is not done by the threads. cmf_10deg(0) # Thread synchronisation barrier. barrier = threading.Barrier(len(gamma_red) + 1) # Help functions for colour interpolation. interpol, purify = None, None # Parameters in `periodically`. fade_ = None # randr is not threadsafe, and absolutely not if you have multiple screens. # drm can be threadsafe but is by default not. # This semaphore is used to make sure that two threads are not accessing randr or drm at the same time. flush_semaphore = threading.Semaphore() def adjust(m): ''' Adjust monitor colours @param m:int The CRTC index ''' while True: # Wait for start cue. barrier.wait() # Calculate temperature. temperature_ = interpol(temperature_day, temperature_night) if fade_ is not None: temperature_ = purify(temperature_, 6500) # Remove settings from last run. start_over() # Apply colour temperature using raw CIE 1964 10 degree CMF data with interpolation. temperature(temperature_, lambda t : divide_by_maximum(cmf_10deg(t))) # Clip colour curves to fit [0, 1] to avoid errors by complex numbers. clip() # Apply gamma correction to monitor. gamma(gamma_red[m], gamma_green[m], gamma_blue[m]) # Flush settings to monitor. flush_semaphore.acquire() (drm if ttymode else randr)(m) flush_semaphore.release() # Signal thread completion. barrier.wait() # Create threads. for m in range(len(gamma_red)): thread = threading.Thread(target = adjust, args = (m,)) thread.setDaemon(True) thread.start() last_dayness = None def periodically(year, month, day, hour, minute, second, weekday, fade): ''' Invoked periodically If you want to control at what to invoke this function next time you can set the value of the global variable `wait_period` to the number of seconds to wait before invoking this function again. The value does not need to be an integer. @param year:int The year @param month:int The month, 1 = January, 12 = December @param day:int The day, minimum value is 1, probable maximum value is 31 (*) @param hour:int The hour, minimum value is 0, maximum value is 23 @param minute:int The minute, minimum value is 0, maximum value is 59 @param second:int The second, minimum value is 0, probable maximum value is 60 (**) @param weekday:int The weekday, 1 = Monday, 7 = Sunday @param fade:float? Blueshift can use this function to fade into a state when it start or exits. `fade` can either be negative, zero or positive or `None`, but the magnitude of value cannot exceed 1. When Blueshift starts, this function will be invoked multiple with the time parameters of the time it is invoked and each time `fade` will increase towards 1, starting at 0, when the value is 1, the settings should be applied to 100 %. After this this function will be invoked once again with `fade` being `None`. When Blueshift exits the same behaviour is used except, `fade` decrease towards -1 but start slightly below 0, when -1 is reached all settings should be normal. Then Blueshift will NOT invoke this function with `fade` being `None`, instead it will by itself revert all settings and quit. (*) Can be exceeded if the calendar system is changed, like in 1712-(02)Feb-30 (**) See https://en.wikipedia.org/wiki/Leap_second ''' global last_dayness, wait_period, interpol, purify, fade_ dayness = get_dayness() # Do not do unnecessary work. if fade is None: if dayness == last_dayness: return last_dayness = dayness # Pass parameters to threads. fade_ = fade # Help functions for colour interpolation. interpol = lambda _day, _night : _day[m % len(_day)] * dayness + _night[m % len(_night)] * (1 - dayness) purify = lambda current, pure : current * abs(fade) + pure * (1 - abs(fade)) # Signal all threads to start. barrier.wait() # Wait for all threads. barrier.wait() def reset(): ''' Invoked to reset the displays ''' for m in range(len(gamma_red)): # Remove settings from last run. start_over() # Apply gamma correction to monitor. gamma(gamma_red[m], gamma_green[m], gamma_blue[m]) # Flush settings to monitor. (drm if ttymode else randr)(m) # Set transition time, 0 on high day and 5 seconds on high night. fadein_time = 5 * (1 - get_dayness()) # Do 10 changes per second. fadein_steps = fadein_time * 10 # Transition on exit in the same way, calculated on exit. old_signal_SIGTERM = signal_SIGTERM if 'SIGTERM' not in conf_storage: conf_storage['SIGTERM'] = old_signal_SIGTERM else: old_signal_SIGTERM = conf_storage['SIGTERM'] def signal_SIGTERM(signum, frame): global fadeout_time, fadeout_steps fadeout_time = 5 * (1 - get_dayness()) fadeout_steps = fadeout_time * 10 old_signal_SIGTERM(signum, frame)