107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
"""Assorted utilities used by drivers and the CLI.
|
||
|
||
Copyright (C) 2018–2019 Jonas Malaco
|
||
Copyright (C) 2018–2019 each contribution's author
|
||
|
||
This file is part of liquidctl.
|
||
|
||
liquidctl 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.
|
||
|
||
liquidctl 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 <https://www.gnu.org/licenses/>.
|
||
"""
|
||
|
||
import logging
|
||
|
||
LOGGER = logging.getLogger(__name__)
|
||
|
||
|
||
def clamp(value, clampmin, clampmax):
|
||
"""Clamp numeric `value` to interval [`clampmin`, `clampmax`]."""
|
||
clamped = max(clampmin, min(clampmax, value))
|
||
if clamped != value:
|
||
LOGGER.debug('clamped %s to interval [%s, %s]', value, clampmin, clampmax)
|
||
return clamped
|
||
|
||
|
||
def delta(profile):
|
||
"""Compute a profile's Δx and Δy."""
|
||
return [(cur[0]-prev[0], cur[1]-prev[1])
|
||
for cur,prev in zip(profile[1:], profile[:-1])]
|
||
|
||
|
||
def normalize_profile(profile, critx):
|
||
"""Normalize a [(x:int, y:int), ...] profile.
|
||
|
||
The normalized profile will ensure that:
|
||
|
||
- the profile is a monotonically increasing function
|
||
(i.e. for every i, i > 1, x[i] - x[i-1] > 0 and y[i] - y[i-1] >= 0)
|
||
- the profile is sorted
|
||
- a (critx, 100) failsafe is enforced
|
||
|
||
>>> normalize_profile([(30, 40), (25, 25), (35, 30), (40, 35), (40, 80)], 60)
|
||
[(25, 25), (30, 40), (35, 40), (40, 80), (60, 100)]
|
||
"""
|
||
profile = sorted(list(profile) + [(critx, 100)], key=lambda p: (p[0], -p[1]))
|
||
mono = profile[0:1]
|
||
for (x, y), (xb, yb) in zip(profile[1:], profile[:-1]):
|
||
if x == xb:
|
||
continue
|
||
if y < yb:
|
||
y = yb
|
||
mono.append((x, y))
|
||
return mono
|
||
|
||
|
||
def interpolate_profile(profile, x):
|
||
"""Interpolate y given x and a [(x: int, y: int), ...] profile.
|
||
|
||
Requires the profile to be sorted by x, with no duplicate x values (see
|
||
normalize_profile). Expects profiles with integer x and y values, and
|
||
returns duty rounded to the nearest integer.
|
||
|
||
>>> interpolate_profile([(20, 50), (50, 70), (60, 100)], 33)
|
||
59
|
||
>>> interpolate_profile([(20, 50), (50, 70)], 19)
|
||
50
|
||
>>> interpolate_profile([(20, 50), (50, 70)], 51)
|
||
70
|
||
>>> interpolate_profile([(20, 50)], 20)
|
||
50
|
||
"""
|
||
lower, upper = profile[0], profile[-1]
|
||
for step in profile:
|
||
if step[0] <= x:
|
||
lower = step
|
||
if step[0] >= x:
|
||
upper = step
|
||
break
|
||
if lower[0] == upper[0]:
|
||
return lower[1]
|
||
return round(lower[1] + (x - lower[0])/(upper[0] - lower[0])*(upper[1] - lower[1]))
|
||
|
||
|
||
def color_from_str(x):
|
||
"""Parse a RGB color from a hexadecimal string.
|
||
|
||
>>> color_from_str('fF7f3f')
|
||
[255, 127, 63]
|
||
|
||
>>> color_from_str('fF7f3f1f')
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: Cannot parse color: fF7f3f1f
|
||
"""
|
||
if len(x) != 6:
|
||
raise ValueError('Cannot parse color: {}'.format(x))
|
||
return list(bytes.fromhex(x))
|