# -*- python -*-
# This example covers most of what Blueshift offers. For a complete
# coverage of Blueshift complement this example with:
# backlight, crtc-detection, crtc-searching, logarithmic,
# stored-settings, modes, textconf, icc-profile-atoms
# However the are features that are only covered by the info manual:
# Methods for calculating correlated colour temperature
# The `functionise` function
# 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 os
# 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
# International Civil Aviation Organization (ICAO)
# code of the nearest airport. Used to get weather
# report. `None` if you do not want to account for
# the weather.
# (Stockholm Bromma Airport in this example.)
airport = 'ESSB'
# Custom dayness by time settings.
time_alpha = [['02:00', 0], ['08:00', 1], ['22:00', 1]]
def by_time():
'''
Dayness calculation using time
'''
global time_alpha
if isinstance(time_alpha[0][0], str):
for i in range(len(time_alpha)):
hh = [float(x) for x in time_alpha[i][0].split(':')]
hh = sum([hh[j] / 60 ** j for j in range(len(hh))])
time_alpha[i][0] = hh
now = datetime.datetime.now()
hh = now.hour + now.minute / 60 + now.second / 60 ** 2
for i in range(len(time_alpha)):
(a, av) = time_alpha[i]
(b, bv) = time_alpha[(i + 1) % len(time_alpha)]
if a < hh: a += 24
if b < hh: b += 24
if a <= hh <= b:
hh = (hh - a) / (b - a)
return av * (1 - hh) + bv * hh
return 1 # Error in `time_alpha` (probably)
# Command used to download a file at an HTTP URL
download_command = None
# This is what if used if `None` is selected:
# download_command = lambda url : ['wget', url, '-O', '-']
# Method for applying colour curves.
apply_curves = randr
#apply_curves = vidmode
if ttymode:
apply_curves = drm
# Keep uncomment to use solar position.
get_dayness = lambda : sun(latitude, longitude)
# Uncomment to use time of day.
#get_dayness = by_time
# Uncomment if you do not want continuous mode, high night values are used.
#get_dayness = None
# Dayness modifiers based on weather and sky conditions.
weather_modifiers = { 'clear' : 1.00
, 'mostly clear' : 0.95
, 'partly cloudy' : 0.90
, 'mostly cloudy' : 0.85
, 'overcast' : 0.80
, 'obscured' : 0.75
}
# The maximum for visibility range for when to
# account for the visibility range. `None` if
# you do dont want to account for visibility range.
visibility_max = 4
# The (zero-based) index of the monitors (CRTC:s) to apply
# settings to. An empty list means that all monitors are used,
# but all monitors will have the same settings.
monitors = []
# The following settings are lists. This is to allow you to
# use different settings on different monitors. For example,
# `gamma_red_day = [1]`, this means that during high day, the
# red gamma is 1 on all monitors. But if we change this to
# `gamma_red_day = [1.0, 1.1]`, the first monitor will have
# the red gamma set to 1,0 and the second monitor will have
# the red gamma set to 1,1. If you have more monitors than
# used in the settings modulo division will be used. For
# instance, if you have four monitors, the third monitor will
# have the same settings as the first monitor, and the fourth
# monitor will have the same settings as the second monitor.
# Colour temperature at high day and high night, respectively.
temperature_day, temperature_night = [6500], [3700]
# Colour brightness at high day and high night, respectively.
# This setting uses the CIE xyY colour space for calculating values.
brightness_day, brightness_night = [1], [1]
# Colour brightness of the red, green and blue components,
# respectively, at high day and high night, respectively.
# This settings uses the sRGB colour space for calculating values.
brightness_red_day, brightness_red_night = [1], [1]
brightness_green_day, brightness_green_night = [1], [1]
brightness_blue_day, brightness_blue_night = [1], [1]
# Colour contrast at high day and high night, respectively.
# This setting uses the CIE xyY colour space for calculating values.
contrast_day, contrast_night = [1], [1]
# Colour contrast of the red, green and blue components,
# respectively, at high day and high night, respectively.
# This settings uses the sRGB colour space for calculating values.
contrast_red_day, contrast_red_night = [1], [1]
contrast_green_day, contrast_green_night = [1], [1]
contrast_blue_day, contrast_blue_night = [1], [1]
# Note: brightness and contrast is not intended for colour
# calibration, it should be calibrated on the monitors'
# control panels.
# Gamma correction for the red, green and blue components, respectively,
# at high day, high night and monitor default, respectively.
# This settings uses the sRGB colour space for calculating values.
gamma_red_day, gamma_red_night, gamma_red_default = [1], [1], [1]
gamma_green_day, gamma_green_night, gamma_green_default = [1], [1], [1]
gamma_blue_day, gamma_blue_night, gamma_blue_default = [1], [1], [1]
# Note: gamma is supposted to be static, it purpose is to
# correct the colours on the monitors the monitor's gamma
# is exactly 2,2 and the colours look correct in relation
# too each other. It is supported to have different settings
# at day and night because there are no technical limitings
# and it can presumable increase readability on text when
# the colour temperature is low.
# Sigmoid curve (S-curve) correction for the red, green and blue
# components, respectively, for each monitor. `None` means that
# no correct is should be applied. `...` means that the value
# above should be used.
sigmoid_red = [None]
sigmoid_green = [...]
sigmoid_blue = [...]
# ICC profile for video filtering and monitor calibration, respectively.
# Replace `None` with the pathname of the profile. It is assume that
# the calibration profile is already applied and that you want it to
# still be applied on exit.
icc_video_filter_profile_day = [None]
icc_video_filter_profile_night = [None]
icc_calibration_profile = [None]
# Function for getting the current the current monitor calibration.
# If `None` the the current monitor calibration will be ignored.
# `if not panicgate:` is included to ignore monitor calibration if
# -p (--panicgate) is used.
current_calibration = [None]
if not panicgate:
if not ttymode:
calib_get = None
#calib_get = randr_get
#calib_get = vidmode_get
else:
calib_get = None
#calib_get = drm_get
current_calibration = [calib_get]
# These are fun curve manipulator settings that lowers the
# colour resolution. `red_x_resolution` is the number of colours
# colours there are one encoding axis of the red curve.
# `red_y_resolution` is how many colours there are on the
# output axis of the red curve. `None` means that the default
# resolution should be used, which are `i_size` for *_x_resolution
# and `o_size` for *_y_resolution. `...` means that the value
# above should be used.
red_x_resolution, red_y_resolution = [None], [None]
green_x_resolution, green_y_resolution = [...], [...]
blue_x_resolution, blue_y_resolution = [...], [...]
# Negative image settings. `None` means that negative image
# is applied to none of the subpixels. `lambda : negative(True)`
# and `negative(True, True, True)` applied negative image to
# all subpixels by reversion the colour curves on the encoding
# axes. For the three parameter functions, the first parameters
# should be `True` to perform negative image on the red subpixel
# and do nothing if `False`, and analogously for green on the
# second parameter and blue on the third parameter. `rgb_invert`
# inverts the curves on the output axes, and `cie_invert` does
# the same thing except it calcuates the inversion in the CIE
# xyY colour space.
negative_image = [None]
#negative_image = [lambda : negative(True)]
#negative_image = [lambda : negative(True, True, True)]
#negative_image = [lambda : rgb_invert(True)]
#negative_image = [lambda : rgb_invert(True, True, True)]
#negative_image = [lambda : cie_invert(True)]
#negative_image = [lambda : cie_invert(True, True, True)]
# Loads the current monitor calibrations.
m = 0
for i in range(len(current_calibration)):
f = current_calibration[i]
if f is not None:
if not len(monitors) == 0:
m = monitors[i % len(monitors)]
current_calibration[i] = f(m)
monitor_controller = lambda : apply_curves(*monitors)
'''
:()→void Function used by Blueshift on exit to apply reset colour curves, if using preimplemented `reset`
'''
uses_adhoc_opts = True
'''
:bool `True` if the configuration script parses the ad-hoc settings
'''
# Get --reset from Blueshift ad-hoc settigns
doreset = parser.opts['--reset']
last_dayness, last_metar = None, None
sigmoid_ = list(zip(sigmoid_red, sigmoid_green, sigmoid_blue))
icc_video_filter_profile = [None] * len(icc_video_filter_profile_day)
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, last_metar, wait_period
dayness = get_dayness()
if airport is not None:
# Get weather report.
(metar, last_time) = (None, None) if last_metar is None else last_metar
now_time = minute
if (metar is None) or (now_time < last_time) or (last_time < now_time + 5):
metar = weather(airport, download_command)
last_metar = (metar, now_time)
# Account for weather.
if metar is not None:
conditions = [metar[0]] + metar[2]
for condition in conditions:
if condition in weather_modifiers:
dayness *= weather_modifiers[condition]
_bound, visibility = metar[1]
if (visibility_max is not None) and (visibility is not None):
if visibility < visibility_max:
dayness *= visibility / visibility_max
# Do not do unnecessary work.
if fade is None:
if dayness == last_dayness:
return
last_dayness = dayness
# 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 * alpha + pure * (1 - alpha)
for m in range(max(1, len(monitors))):
temperature_ = interpol(temperature_day, temperature_night)
brightness_ = interpol(brightness_day, brightness_night)
brightness_red_ = interpol(brightness_red_day, brightness_red_night)
brightness_green_ = interpol(brightness_green_day, brightness_green_night)
brightness_blue_ = interpol(brightness_blue_day, brightness_blue_night)
contrast_ = interpol(contrast_day, contrast_night)
contrast_red_ = interpol(contrast_red_day, contrast_red_night)
contrast_green_ = interpol(contrast_green_day, contrast_green_night)
contrast_blue_ = interpol(contrast_blue_day, contrast_blue_night)
gamma_red_ = interpol(gamma_red_day, gamma_red_night)
gamma_green_ = interpol(gamma_green_day, gamma_green_night)
gamma_blue_ = interpol(gamma_blue_day, gamma_blue_night)
if fade is not None:
alpha = abs(fade)
temperature_ = purify(temperature_, 6500)
brightness_ = purify(brightness_, 1)
brightness_red_ = purify(brightness_red_, 1)
brightness_green_ = purify(brightness_green_, 1)
brightness_blue_ = purify(brightness_blue_, 1)
contrast_ = purify(contrast_, 1)
contrast_red_ = purify(contrast_red_, 1)
contrast_green_ = purify(contrast_green_, 1)
contrast_blue_ = purify(contrast_blue_, 1)
gamma_red_ = purify(gamma_red_, gamma_red_default [m % len(gamma_red_default)])
gamma_green_ = purify(gamma_green_, gamma_green_default[m % len(gamma_green_default)])
gamma_blue_ = purify(gamma_blue_, gamma_blue_default [m % len(gamma_blue_default)])
# Remove settings from last run.
start_over()
# Apply ICC profile as a video filter.
i = m % len(icc_video_filter_profile)
if icc_video_filter_profile_day[i] is not None:
if icc_video_filter_profile[i] is None:
day = load_icc(icc_video_filter_profile_day[i])
night = load_icc(icc_video_filter_profile_night[i])
icc_video_filter_profile[i] = make_icc_interpolation([night, day])
icc_video_filter_profile[i](dayness, 1 if fade is None else abs(fade))
# Apply negative image.
f = negative_image[m % len(negative_image)]
if f is not None:
f()
# Apply colour temperature using raw CIE 1964 10 degree CMF data with interpolation.
temperature(temperature_, lambda t : clip_whitepoint(divide_by_maximum(cmf_10deg(t))))
# Apply calibration used when started.
c = current_calibration[m % len(current_calibration)]
if c is not None:
c()
# Apply colour brightness using the CIE xyY colour space.
cie_brightness(brightness_)
# Apply colour brightness using the sRGB colour space.
# If we only used one parameter, it would be applied to all colour components.
rgb_brightness(brightness_red_, brightness_green_, brightness_blue_)
# Apply colour contrast using the CIE xyY colour space.
cie_contrast(contrast_)
# Apply colour contrast using the sRGB colour space.
# If we only used one parameter, it would be applied to all colour components.
rgb_contrast(contrast_red_, contrast_green_, contrast_blue_)
# Apply low colour resolution emulation.
rx = red_x_resolution[m % len(red_x_resolution)]
ry = red_y_resolution[m % len(red_y_resolution)]
gx = green_x_resolution[m % len(green_x_resolution)]
gy = green_y_resolution[m % len(green_y_resolution)]
bx = blue_x_resolution[m % len(blue_x_resolution)]
by = blue_y_resolution[m % len(blue_y_resolution)]
lower_resolution(rx, ry, gx, gy, bx, by)
# Clip colour curves to fit [0, 1] to avoid errors by complex numbers.
clip()
# Apply gamma correction to monitor.
gamma(gamma_red_, gamma_green_, gamma_blue_)
# Apply sigmoid curve correction to monitor.
sigmoid(*(sigmoid_[m % len(sigmoid_)]))
# Apply ICC profile as a monitor calibration.
i = m % len(icc_calibration_profile)
if icc_calibration_profile[i] is not None:
if isinstance(icc_calibration_profile[i], str):
icc_calibration_profile[i] = load_icc(icc_calibration_profile[i])
icc_calibration_profile[i]()
# Flush settings to monitor.
if len(monitors) == 0:
apply_curves()
else:
apply_curves(monitors[m % len(monitors)])
# Lets wait only 5 seconds, instead of a minute before running again.
wait_period = 5
def reset():
'''
Invoked to reset the displays
'''
for m in range(max(1, len(monitors))):
gamma_red_ = gamma_red_default [m % len(gamma_red_default)]
gamma_green_ = gamma_green_default[m % len(gamma_green_default)]
gamma_blue_ = gamma_blue_default [m % len(gamma_blue_default)]
# Remove settings from last run.
start_over()
# Apply calibration used when started.
c = current_calibration[m % len(current_calibration)]
if c is not None:
c()
# Apply gamma correction to monitor.
gamma(gamma_red_, gamma_green_, gamma_blue_)
# Apply ICC profile as a monitor calibration.
i = m % len(icc_calibration_profile)
if icc_calibration_profile[i] is not None:
if isinstance(icc_calibration_profile[i], str):
icc_calibration_profile[i] = load_icc(icc_calibration_profile[i])
icc_calibration_profile[i]()
# Flush settings to monitor.
if len(monitors) == 0:
apply_curves()
else:
apply_curves(monitors[m % len(monitors)])
if (get_dayness is not None) and not doreset:
# 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)
else:
# Do not use continuous mode.
get_dayness = lambda : 0
def apply(fade):
t = datetime.datetime.now()
wd = t.isocalendar()[2]
periodically(t.year, t.month, t.day, t.hour, t.minute, t.second, wd, fade)
if not panicgate:
signal.signal(signal.SIGTERM, signal_SIGTERM)
trans = 0
apply((1 - trans) if doreset else trans)
while running:
time.sleep(0.1)
if trans >= 1:
break
trans += 0.05
apply((1 - trans) if doreset else trans)
if not doreset:
apply(None)
else:
reset()
periodically = None