Skip to content

Commit

Permalink
finish lights refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
jabdoa2 committed Mar 11, 2017
1 parent f391c90 commit 231c70d
Show file tree
Hide file tree
Showing 23 changed files with 367 additions and 200 deletions.
29 changes: 2 additions & 27 deletions mpf/core/light_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ def __init__(self, machine):
# Generate and add color correction profiles to the machine
self.light_color_correction_profiles = dict()

self.lights_to_update = set()
# will only get initialised if there are lights
self._initialised = False
self._updater_task = None

def initialise_light_subsystem(self):
"""Initialise the light subsystem."""
Expand Down Expand Up @@ -53,30 +52,6 @@ def initialise_light_subsystem(self):
linear_cutoff=profile_parameters['linear_cutoff'])
self.light_color_correction_profiles[profile_name] = profile

# schedule the single machine-wide update to write the current light of
# each light to the hardware
self._updater_task = self.machine.clock.schedule_interval(
self._update_lights, 1 / self.machine.config['mpf']['default_light_hw_update_hz'])

# add setting for brightness
self.machine.settings.add_setting(SettingEntry("brightness", "Brightness", 100, "brightness", 1.0,
{0.25: "25%", 0.5: "50%", 0.75: "75%", 1.0: "100% (default)"}))

def _update_lights(self, dt):
"""Write lights to hardware platform.
Called periodically (default at the end of every frame) to write the
new light colors to the hardware for the lights that changed during that
frame.
Args:
dt: time since last call
"""
del dt

new_lights_to_update = set()
if self.lights_to_update:
for light in self.lights_to_update:
light.write_color_to_hw_driver()
if light.fade_in_progress:
new_lights_to_update.add(light)
self.lights_to_update = new_lights_to_update
2 changes: 1 addition & 1 deletion mpf/core/platform.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Contains the parent class for all platforms."""
import abc

from typing import Optional
from typing import Optional, Callable, Tuple

from mpf.devices.switch import Switch
from mpf.platforms.interfaces.light_platform_interface import LightPlatformInterface
Expand Down
100 changes: 62 additions & 38 deletions mpf/devices/light.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
"""Contains the Light class."""
from functools import partial
from operator import itemgetter
from typing import Tuple

from mpf.core.device_monitor import DeviceMonitor
from mpf.core.rgb_color import RGBColor
from mpf.core.system_wide_device import SystemWideDevice
from mpf.devices.driver import ReconfiguredDriver
from mpf.platforms.interfaces.light_platform_interface import LightPlatformInterface
from mpf.platforms.interfaces.light_platform_interface import LightPlatformSoftwareFade


class DriverLight(LightPlatformInterface):
class DriverLight(LightPlatformSoftwareFade):

"""A coil which is used to drive a light."""

def __init__(self, driver):
def __init__(self, driver, loop, software_fade_ms):
"""Initialise coil as light."""
super().__init__(loop, software_fade_ms)
self.driver = driver

def set_brightness(self, brightness: float, fade_ms: int):
def set_brightness(self, brightness: float):
"""Set pwm to coil."""
# TODO: fix driver interface
if brightness <= 0:
Expand All @@ -43,9 +46,7 @@ def __init__(self, machine, name):
super().__init__(machine, name)
self.machine.light_controller.initialise_light_subsystem()

self.fade_in_progress = False
self.default_fade_ms = None
self._max_fade_ms = 0

self._color_correction_profile = None

Expand Down Expand Up @@ -152,18 +153,15 @@ def _load_hw_drivers(self):
if not channels:
raise AssertionError("Light {} has no channels.".format(self.name))

max_fade_ms = []
for num, channel in channels.items():
for color, channel in channels.items():
channel = self.machine.config_validator.validate_config("light_channels", channel)
self.hw_drivers[num] = self._load_hw_driver(channel)
max_fade_ms.append(self.hw_drivers[num].get_max_fade_ms())
self.hw_drivers[color] = self._load_hw_driver(channel, color)

self._max_fade_ms = min(max_fade_ms)

def _load_hw_driver(self, channel):
def _load_hw_driver(self, channel, color):
"""Load one channel."""
if channel['platform'] == "drivers":
return DriverLight(self.machine.coils[channel['number'].strip()])
return DriverLight(self.machine.coils[channel['number'].strip()], self.machine.clock.loop,
int(1 / self.machine.config['mpf']['default_light_hw_update_hz'] * 1000))
else:
platform = self.machine.get_platform_sections('lights', channel['platform'])
return platform.configure_light(channel['number'], channel['subtype'], channel['platform_settings'])
Expand Down Expand Up @@ -277,7 +275,7 @@ def off(self, fade_ms=None, priority=0, key=None, **kwargs):
def _add_to_stack(self, color, fade_ms, priority, key):
curr_color = self.get_color()

self.remove_from_stack_by_key(key)
self._remove_from_stack_by_key(key)

if fade_ms:
new_color = curr_color
Expand Down Expand Up @@ -318,15 +316,16 @@ def remove_from_stack_by_key(self, key):
were removed, the light will be updated with whatever's below it. If no
settings remain after these are removed, the light will turn off.
"""
self.debug_log("Removing key '%s' from stack", key)
self._remove_from_stack_by_key(key)
self._schedule_update()

def _remove_from_stack_by_key(self, key):
self.debug_log("Removing key '%s' from stack", key)
self.stack[:] = [x for x in self.stack if x['key'] != key]

self._schedule_update()

def _schedule_update(self):
self.fade_in_progress = self.stack and self.stack[0]['dest_time']
self.machine.light_controller.lights_to_update.add(self)
for color, hw_driver in self.hw_drivers.items():
hw_driver.set_fade(partial(self._get_brightness_and_fade, color=color))

def clear_stack(self):
"""Remove all entries from the stack and resets this light to 'off'."""
Expand Down Expand Up @@ -381,39 +380,64 @@ def color_correct(self, color):

return self._color_correction_profile.apply(color)

def get_color(self):
"""Return an RGBColor() instance of the 'color' setting of the highest color setting in the stack.
This is usually the same color as the physical light, but not always (since physical lights are updated once per
frame, this value could vary.
Also note the color returned is the "raw" color that does has not had the color correction profile applied.
"""
def _get_color_and_fade(self, max_fade_ms: int) -> Tuple[RGBColor, int]:
try:
color_settings = self.stack[0]
except IndexError:
# no stack
return RGBColor('off')
return RGBColor('off'), -1

# no fade
if not color_settings['dest_time']:
return color_settings['dest_color']
return color_settings['dest_color'], -1

current_time = self.machine.clock.get_time()

# fade is done
if current_time >= color_settings['dest_time']:
return color_settings['dest_color'], -1

target_time = current_time + (max_fade_ms / 1000.0)
# check if fade will be done before max_fade_ms
if target_time > color_settings['dest_time']:
return color_settings['dest_time'], int(color_settings['dest_time'] - current_time) / 1000

# figure out the ratio of how far along we are
try:
ratio = ((self.machine.clock.get_time() -
color_settings['start_time']) /
(color_settings['dest_time'] -
color_settings['start_time']))
ratio = ((target_time - color_settings['start_time']) /
(color_settings['dest_time'] - color_settings['start_time']))
except ZeroDivisionError:
ratio = 1.0

self.debug_log("Fade, ratio: %s", ratio)
return RGBColor.blend(color_settings['start_color'], color_settings['dest_color'], ratio), max_fade_ms

def _get_brightness_and_fade(self, max_fade_ms: int, color: str) -> Tuple[float, int]:
uncorrected_color, fade_ms = self._get_color_and_fade(max_fade_ms)
corrected_color = self.gamma_correct(uncorrected_color)
corrected_color = self.color_correct(corrected_color)

if ratio >= 1.0: # fade is done
return color_settings['dest_color']
if color in ["red", "blue", "green"]:
brightness = getattr(corrected_color, color) / 255.0
elif color == "white":
brightness = min(corrected_color.red, corrected_color.green, corrected_color.blue) / 255.0
else:
return RGBColor.blend(color_settings['start_color'], color_settings['dest_color'], ratio)
raise AssertionError("Invalid color {}".format(color))
return brightness, fade_ms

def get_color(self):
"""Return an RGBColor() instance of the 'color' setting of the highest color setting in the stack.
This is usually the same color as the physical light, but not always (since physical lights are updated once per
frame, this value could vary.
Also note the color returned is the "raw" color that does has not had the color correction profile applied.
"""
return self._get_color_and_fade(0)[0]

@property
def fade_in_progress(self) -> bool:
"""Return true if a fade is in progress."""
return bool(self.stack and self.stack[0]['dest_time'] > self.machine.clock.get_time())

def write_color_to_hw_driver(self):
"""Set color to hardware platform.
Expand Down
17 changes: 11 additions & 6 deletions mpf/platforms/fast/fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,11 @@ def update_leds(self, dt):
dt: time since last call
"""
del dt
msg = 'RS:' + ','.join(["%s%s" % (led.number, led.current_color)
for led in self.fast_leds.values()])
self.rgb_connection.send(msg)
dirty_leds = [led for led in self.fast_leds.values() if led.dirty]

if dirty_leds:
msg = 'RS:' + ','.join(["%s%s" % (led.number, led.current_color) for led in dirty_leds])
self.rgb_connection.send(msg)

def get_hw_switch_states(self):
"""Return hardware states."""
Expand Down Expand Up @@ -477,9 +479,11 @@ def configure_light(self, number, subtype, platform_settings) -> LightPlatformIn
'but no connection to a NET processor is '
'available')
if subtype == "gi":
return FASTGIString(number, self.net_connection.send)
return FASTGIString(number, self.net_connection.send, self.machine,
int(1 / self.machine.config['mpf']['default_light_hw_update_hz'] * 1000))
elif subtype == "matrix":
return FASTMatrixLight(number, self.net_connection.send)
return FASTMatrixLight(number, self.net_connection.send, self.machine,
int(1 / self.machine.config['mpf']['default_light_hw_update_hz'] * 1000))
elif not subtype or subtype == "led":
if not self.flag_led_tick_registered:
# Update leds every frame
Expand All @@ -489,7 +493,8 @@ def configure_light(self, number, subtype, platform_settings) -> LightPlatformIn

number_str, channel = number.split("-")
if number_str not in self.fast_leds:
self.fast_leds[number_str] = FASTDirectLED(number_str)
self.fast_leds[number_str] = FASTDirectLED(
number_str, int(self.config['hardware_led_fade_time']))
fast_led_channel = FASTDirectLEDChannel(self.fast_leds[number_str], channel)

return fast_led_channel
Expand Down
18 changes: 7 additions & 11 deletions mpf/platforms/fast/fast_gi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,22 @@
import logging

from mpf.core.utility_functions import Util
from mpf.platforms.interfaces.light_platform_interface import LightPlatformInterface
from mpf.platforms.interfaces.light_platform_interface import LightPlatformSoftwareFade


class FASTGIString(LightPlatformInterface):
class FASTGIString(LightPlatformSoftwareFade):

"""A FAST GI string in a WPC machine."""

def __init__(self, number, sender):
"""Initialise GI string.
TODO: Need to implement the enable_relay and control which strings are
dimmable.
"""
def __init__(self, number, sender, machine, software_fade_ms: int):
"""Initialise GI string."""
super().__init__(machine.clock.loop, software_fade_ms)
self.log = logging.getLogger('FASTGIString.0x' + str(number))
self.number = number
self.send = sender

def set_brightness(self, brightness: float, fade_ms: int):
"""Turn on GI string."""
del fade_ms
def set_brightness(self, brightness: float):
"""Set GI string to a certain brightness."""
brightness = int(brightness * 255)
if brightness >= 255:
brightness = 255
Expand Down
30 changes: 21 additions & 9 deletions mpf/platforms/fast/fast_led.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"""WS2812 LED on the FAST controller."""
import logging

from typing import Callable, Tuple

from mpf.platforms.interfaces.light_platform_interface import LightPlatformInterface


class FASTDirectLED:

"""FAST RGB LED."""

def __init__(self, number):
def __init__(self, number: str, hardware_fade_ms: int):
"""Initialise FAST LED."""
self.number = number
self.dirty = True
self.hardware_fade_ms = hardware_fade_ms
self.colors = [0, 0, 0]
self.log = logging.getLogger('FASTLED')
# All FAST LEDs are 3 element RGB and are set using hex strings
Expand All @@ -19,9 +23,18 @@ def __init__(self, number):
@property
def current_color(self):
"""Return current color."""
return "{0}{1}{2}".format(hex(int(self.colors[0]))[2:].zfill(2),
hex(int(self.colors[1]))[2:].zfill(2),
hex(int(self.colors[2]))[2:].zfill(2))
result = ""
self.dirty = False
for color in self.colors:
if callable(color):
brightness, fade_ms = color(self.hardware_fade_ms)
result += hex(int(brightness * 255))[2:].zfill(2)
if fade_ms >= self.hardware_fade_ms:
self.dirty = True
else:
result += "00"

return result


class FASTDirectLEDChannel(LightPlatformInterface):
Expand All @@ -33,8 +46,7 @@ def __init__(self, led: FASTDirectLED, channel):
self.led = led
self.channel = int(channel)

def set_brightness(self, brightness: float, fade_ms: int):
"""Instantly set this LED channel to the brightness passed."""
# FAST does not support fade per light/channel
del fade_ms
self.led.colors[self.channel] = int(brightness * 255)
def set_fade(self, color_and_fade_callback: Callable[[int], Tuple[float, int]]):
"""Set brightness via callback."""
self.led.dirty = True
self.led.colors[self.channel] = color_and_fade_callback
13 changes: 6 additions & 7 deletions mpf/platforms/fast/fast_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@
import logging

from mpf.core.utility_functions import Util
from mpf.platforms.interfaces.light_platform_interface import LightPlatformInterface
from mpf.platforms.interfaces.light_platform_interface import LightPlatformSoftwareFade


class FASTMatrixLight(LightPlatformInterface):
class FASTMatrixLight(LightPlatformSoftwareFade):

"""A direct light on a fast controller."""

def __init__(self, number, sender):
def __init__(self, number, sender, machine, fade_interval_ms: int):
"""Initialise light."""
super().__init__(machine.clock.loop, fade_interval_ms)
self.log = logging.getLogger('FASTMatrixLight')
self.number = number
self.send = sender

def set_brightness(self, brightness: float, fade_ms: int):
"""Enable (turn on) this driver."""
# FAST gi does not support fades
del fade_ms
def set_brightness(self, brightness: float):
"""Set matrix light brightness."""
self.send('L1:{},{}'.format(self.number, Util.int_to_hex_string(int(brightness * 255))))

0 comments on commit 231c70d

Please sign in to comment.