Example #1
0
class Lights(Stateful):
    """This class manages the updating of visualizations
    and provides endpoints to control them."""

    UPS = 30

    def __init__(self, base: "Base") -> None:
        self.seconds_per_frame = 1 / self.UPS

        self.base = base
        self.ring = Ring()
        self.strip = Strip()
        self.screen = Screen()

        # if the led loop is running
        self.loop_active = threading.Event()

        self.disabled_program = Disabled(self)
        self.cava_program = Cava(self)
        self.alarm_program = Alarm(self)
        # a dictionary containing all led programs by their name
        self.led_programs: Dict[str, LedProgram] = {
            "Disabled": self.disabled_program
        }
        for led_program_class in [Fixed, Rainbow, Adaptive]:
            led_instance = led_program_class(self)
            self.led_programs[led_instance.name] = led_instance
        self.screen_programs: Dict[str, ScreenProgram] = {
            "Disabled": self.disabled_program
        }
        for screen_program_class in [Circle]:
            screen_instance = screen_program_class(self)
            self.screen_programs[screen_instance.name] = screen_instance

        # this lock ensures that only one thread changes led options
        self.option_lock = threading.Lock()
        self.program_speed = 1.0
        self.fixed_color = (0.0, 0.0, 0.0)
        self.last_fixed_color = self.fixed_color

        last_ring_program_name = self.base.settings.get_setting(
            "last_ring_program", "Disabled")
        last_strip_program_name = self.base.settings.get_setting(
            "last_strip_program", "Disabled")
        last_screen_program_name = self.base.settings.get_setting(
            "last_screen_program", "Disabled")
        ring_program_name = self.base.settings.get_setting(
            "ring_program", "Disabled")
        strip_program_name = self.base.settings.get_setting(
            "strip_program", "Disabled")
        screen_program_name = self.base.settings.get_setting(
            "screen_program", "Disabled")

        self.last_ring_program = self.led_programs[last_ring_program_name]
        self.last_strip_program = self.led_programs[last_strip_program_name]
        self.last_screen_program = self.screen_programs[
            last_screen_program_name]

        self.ring_program = self.led_programs[ring_program_name]
        self.strip_program = self.led_programs[strip_program_name]
        self.screen_program = self.screen_programs[screen_program_name]
        # disable disconnected devices
        if not self.ring.initialized:
            self.ring_program = self.disabled_program
        if not self.strip.initialized:
            self.strip_program = self.disabled_program
        if not self.screen.initialized:
            self.screen_program = self.disabled_program
        self.ring_program.use()
        self.strip_program.use()
        self.screen_program.use()
        self._consumers_changed()

        self._loop()

    @background_thread
    def _loop(self) -> None:
        iteration_count = 0
        adaptive_quality_window = self.UPS * 10
        time_sum = 0.0
        while True:
            self.loop_active.wait()

            with self.option_lock:
                computation_start = time.time()

                # these programs only actually do work if their respective programs are active
                self.cava_program.compute()
                self.alarm_program.compute()

                if self.screen_program.name != "Disabled":
                    self.screen_program.draw()

                self.ring_program.compute()
                if self.strip_program != self.ring_program:
                    self.strip_program.compute()

                if self.ring_program.name != "Disabled":
                    if self.ring.monochrome:
                        ring_colors = [
                            self.ring_program.strip_color()
                            for _ in range(self.ring.LED_COUNT)
                        ]
                    else:
                        ring_colors = self.ring_program.ring_colors()
                    self.ring.set_colors(ring_colors)

                if self.strip_program.name != "Disabled":
                    strip_color = self.strip_program.strip_color()
                    self.strip.set_color(strip_color)

            computation_time = time.time() - computation_start

            if self.screen_program.name != "Disabled":
                time_sum += computation_time
                iteration_count += 1
                if (iteration_count >= adaptive_quality_window
                        or time_sum >= 1.5 * adaptive_quality_window *
                        self.seconds_per_frame):
                    average_computation_time = time_sum / adaptive_quality_window
                    iteration_count = 0
                    time_sum = 0.0

                    # print(f"avg: {average_computation_time/self.seconds_per_frame}")
                    if average_computation_time > 0.9 * self.seconds_per_frame:
                        # if the loop takes too long and a screen program is active,
                        # it can be reduced in resolution to save time
                        self.screen_program.decrease_resolution()
                    elif average_computation_time < 0.6 * self.seconds_per_frame:
                        # if the loop has time to spare and a screen program is active,
                        # we can increase its quality
                        self.screen_program.increase_resolution()

            # print(f'computation took {computation_time:.5f}s')
            try:
                time.sleep(self.seconds_per_frame - computation_time)
            except ValueError:
                pass

    def _consumers_changed(self) -> None:
        if self.disabled_program.consumers == 3:
            self.loop_active.clear()
        else:
            self.loop_active.set()

    def _set_ring_program(self,
                          program: LedProgram,
                          transient: bool = False) -> None:
        # don't allow program change on disconnected devices
        if not self.ring.initialized:
            return

        self.ring_program.release()
        program.use()

        self.last_ring_program = self.ring_program
        self.ring_program = program
        if not transient:
            Setting.objects.filter(key="last_ring_program").update(
                value=self.last_ring_program.name)
            Setting.objects.filter(key="ring_program").update(
                value=self.ring_program.name)
        self._consumers_changed()

        if program.name == "Disabled":
            self.ring.clear()

    def _set_strip_program(self,
                           program: LedProgram,
                           transient: bool = False) -> None:
        # don't allow program change on disconnected devices
        if not self.strip.initialized:
            return

        self.strip_program.release()
        program.use()

        self.last_strip_program = self.strip_program
        self.strip_program = program
        if not transient:
            Setting.objects.filter(key="last_strip_program").update(
                value=self.last_strip_program.name)
            Setting.objects.filter(key="strip_program").update(
                value=self.strip_program.name)
        self._consumers_changed()

        if program.name == "Disabled":
            self.strip.clear()

    def _set_screen_program(self,
                            program: ScreenProgram,
                            transient: bool = False):
        if not self.screen.initialized:
            return
        self.screen_program.release()
        program.use()

        self.last_screen_program = self.screen_program
        self.screen_program = program
        if not transient:
            Setting.objects.filter(key="last_screen_program").update(
                value=self.last_screen_program.name)
            Setting.objects.filter(key="screen_program").update(
                value=self.screen_program.name)
        self._consumers_changed()

    def alarm_started(self) -> None:
        """Makes alarm the current program but doesn't update the database."""
        with self.option_lock:
            self.alarm_program.use()
            self.last_fixed_color = self.fixed_color
            self._set_ring_program(self.led_programs["Fixed"], transient=True)
            self._set_strip_program(self.led_programs["Fixed"], transient=True)
            # the screen program adapts with the alarm and is not changed

    def alarm_stopped(self) -> None:
        """Restores the state from before the alarm."""
        with self.option_lock:
            self.alarm_program.release()
            self.fixed_color = self.last_fixed_color
            self._set_ring_program(self.last_ring_program, transient=True)
            self._set_strip_program(self.last_strip_program, transient=True)

            # read last programs from database, which is still in the state before the alarm
            last_ring_program_name = Setting.objects.get(
                key="last_ring_program").value
            last_strip_program_name = Setting.objects.get(
                key="last_strip_program").value
            self.last_ring_program = self.led_programs[last_ring_program_name]
            self.last_strip_program = self.led_programs[
                last_strip_program_name]

    def state_dict(self) -> Dict[str, Any]:
        state_dict = self.base.state_dict()
        state_dict["ring_connected"] = self.ring.initialized
        state_dict["ring_program"] = self.ring_program.name
        state_dict["ring_brightness"] = self.ring.brightness
        state_dict["ring_monochrome"] = self.ring.monochrome
        state_dict["strip_connected"] = self.strip.initialized
        state_dict["strip_program"] = self.strip_program.name
        state_dict["strip_brightness"] = self.strip.brightness
        state_dict["screen_connected"] = self.screen.initialized
        state_dict["screen_program"] = self.screen_program.name
        state_dict["program_speed"] = self.program_speed
        state_dict["fixed_color"] = "#{:02x}{:02x}{:02x}".format(
            *(int(val * 255) for val in self.fixed_color))
        return state_dict

    def index(self, request: WSGIRequest) -> HttpResponse:
        """Renders the /lights page."""
        context = self.base.context(request)
        # programs that have a strip_color or ring_color function are color programs
        # programs that have a draw function are screen programs
        context["color_program_names"] = [
            program.name for program in self.led_programs.values()
        ]
        context["screen_program_names"] = [
            program.name for program in self.screen_programs.values()
        ]
        # context['program_names'].remove('Alarm')
        return render(request, "lights.html", context)

    @option
    def set_lights_shortcut(self, request: WSGIRequest) -> None:
        """Stores the current lights state and restores the previous one."""
        should_enable = request.POST.get("value") == "true"
        is_enabled = (self.ring_program.name != "Disabled"
                      or self.strip_program.name != "Disabled")
        if should_enable == is_enabled:
            return
        if should_enable:
            self._set_ring_program(self.last_ring_program)
            self._set_strip_program(self.last_strip_program)
        else:
            self._set_ring_program(self.disabled_program)
            self._set_strip_program(self.disabled_program)

    @option
    def set_ring_program(self, request: WSGIRequest) -> None:
        """Updates the ring program."""
        program_name = request.POST.get("program")
        if not program_name:
            return
        program = self.led_programs[program_name]
        if program == self.ring_program:
            # the program doesn't change, return immediately
            return
        self._set_ring_program(program)

    @option
    def set_ring_brightness(self, request: WSGIRequest) -> None:
        """Updates the ring brightness."""
        # raises ValueError on wrong input, caught in option decorator
        value = float(request.POST.get("value"))  # type: ignore
        self.ring.brightness = value

    @option
    def set_ring_monochrome(self, request: WSGIRequest) -> None:
        """Sets whether the ring should be in one color only."""
        enabled = request.POST.get("value") == "true"  # type: ignore
        self.ring.monochrome = enabled

    @option
    def set_strip_program(self, request: WSGIRequest) -> None:
        """Updates the strip program."""
        program_name = request.POST.get("program")
        program = self.led_programs[program_name]  # type: ignore
        if program == self.strip_program:
            # the program doesn't change, return immediately
            return
        self._set_strip_program(program)

    @option
    def set_strip_brightness(self, request: WSGIRequest) -> None:
        """Updates the strip brightness."""
        # raises ValueError on wrong input, caught in option decorator
        value = float(request.POST.get("value"))  # type: ignore
        self.strip.brightness = value

    @option
    def adjust_screen(self, _request: WSGIRequest) -> Optional[HttpResponse]:
        """Adjusts the resolution of the screen."""
        if self.screen_program.name != "Disabled":
            return HttpResponseBadRequest(
                "Disable the screen program before readjusting")
        self.screen.adjust()
        return HttpResponse()

    @option
    def set_screen_program(self, request: WSGIRequest) -> None:
        """Updates the screen program."""
        program_name = request.POST.get("program")
        program = self.screen_programs[program_name]  # type: ignore
        if program == self.screen_program:
            # the program doesn't change, return immediately
            return
        self._set_screen_program(program)

    @option
    def set_program_speed(self, request: WSGIRequest) -> None:
        """Updates the global speed of programs supporting it."""
        value = float(request.POST.get("value"))  # type: ignore
        self.program_speed = value

    @option
    def set_fixed_color(self, request: WSGIRequest) -> None:
        """Updates the static color used for some programs."""
        hex_col = request.POST.get("value").lstrip("#")  # type: ignore
        # raises IndexError on wrong input, caught in option decorator
        color = tuple(int(hex_col[i:i + 2], 16) / 255 for i in (0, 2, 4))
        # https://github.com/python/mypy/issues/5068
        color = cast(Tuple[float, float, float], color)
        self.fixed_color = color
Example #2
0
class Lights:
    def __init__(self, base):
        self.UPS = 30
        self.seconds_per_frame = 1 / self.UPS

        self.base = base
        self.ring = Ring()
        self.strip = Strip()
        self.screen = Screen()

        # if the led loop is running
        self.loop_active = threading.Event()

        self.cava_program = Cava(self)
        self.alarm_program = Alarm(self)
        # a dictionary containing all programs by their name
        self.programs = {}
        for program_class in [
                Disabled, Fixed, Alarm, Rainbow, Adaptive, Circle
        ]:
            instance = program_class(self)
            self.programs[instance.name] = instance

        # this lock ensures that only one thread changes led options
        self.option_lock = threading.Lock()
        self.program_speed = 1

        last_ring_program_name = Setting.objects.get_or_create(
            key='last_ring_program', defaults={'value': 'Disabled'})[0].value
        last_strip_program_name = Setting.objects.get_or_create(
            key='last_strip_program', defaults={'value': 'Disabled'})[0].value
        last_screen_program_name = Setting.objects.get_or_create(
            key='last_screen_program', defaults={'value': 'Disabled'})[0].value
        ring_program_name = Setting.objects.get_or_create(
            key='ring_program', defaults={'value': 'Disabled'})[0].value
        strip_program_name = Setting.objects.get_or_create(
            key='strip_program', defaults={'value': 'Disabled'})[0].value
        screen_program_name = Setting.objects.get_or_create(
            key='screen_program', defaults={'value': 'Disabled'})[0].value

        self.last_ring_program = self.programs[last_ring_program_name]
        self.last_strip_program = self.programs[last_strip_program_name]
        self.last_screen_program = self.programs[last_screen_program_name]

        self.ring_program = self.programs[ring_program_name]
        self.strip_program = self.programs[strip_program_name]
        self.screen_program = self.programs[screen_program_name]
        # disable disconnected devices
        if not self.ring.initialized:
            self.ring_program = self.programs['Disabled']
        if not self.strip.initialized:
            self.strip_program = self.programs['Disabled']
        if not self.screen.initialized:
            self.screen_program = self.programs['Disabled']
        self.ring_program.use()
        self.strip_program.use()
        self.screen_program.use()
        self.consumers_changed()

        self.start()

    def start(self):
        threading.Thread(target=self._loop, daemon=True).start()

    def _loop(self):
        iteration_count = 0
        adaptive_quality_window = self.UPS * 10
        time_sum = 0
        while True:
            self.loop_active.wait()

            with self.option_lock:
                computation_start = time.time()

                # these programs only actually compute something if they are used, so these functions are always called
                self.cava_program.compute()
                self.alarm_program.compute()

                if self.screen_program.name != 'Disabled':
                    self.screen_program.draw()

                self.ring_program.compute()
                if self.strip_program != self.ring_program:
                    self.strip_program.compute()

                if self.ring_program.name != 'Disabled':
                    if self.ring.monochrome:
                        ring_colors = [
                            self.ring_program.strip_color()
                            for led in range(self.ring.LED_COUNT)
                        ]
                    else:
                        ring_colors = self.ring_program.ring_colors()
                    self.ring.set_colors(ring_colors)

                if self.strip_program.name != 'Disabled':
                    strip_color = self.strip_program.strip_color()
                    self.strip.set_color(strip_color)

            computation_time = time.time() - computation_start

            if self.screen_program.name != 'Disabled':
                time_sum += computation_time
                iteration_count += 1
                if iteration_count >= adaptive_quality_window or time_sum >= 1.5 * adaptive_quality_window * self.seconds_per_frame:
                    average_computation_time = time_sum / adaptive_quality_window
                    iteration_count = 0
                    time_sum = 0

                    #print(f'average compute ratio: {average_computation_time/self.seconds_per_frame}')
                    if average_computation_time > 0.9 * self.seconds_per_frame:
                        # if the loop takes too long and a screen program is active, it can be reduced in resolution to save time
                        self.screen_program.decrease_resolution()
                    elif average_computation_time < 0.6 * self.seconds_per_frame:
                        # if the loop has time to spare and a screen program is active, we can increase its quality
                        self.screen_program.increase_resolution()

            #print(f'computation took {computation_time:.5f}s')
            try:
                time.sleep(self.seconds_per_frame - computation_time)
            except ValueError:
                pass

    def consumers_changed(self):
        if self.programs['Disabled'].consumers == 3:
            self.loop_active.clear()
        else:
            self.loop_active.set()

    def _set_ring_program(self, program, transient=False):
        # don't allow program change on disconnected devices
        if not self.ring.initialized:
            return

        self.ring_program.release()
        program.use()

        self.last_ring_program = self.ring_program
        self.ring_program = program
        if not transient:
            Setting.objects.filter(key='last_ring_program').update(
                value=self.last_ring_program.name)
            Setting.objects.filter(key='ring_program').update(
                value=self.ring_program.name)
        self.consumers_changed()

        if program.name == 'Disabled':
            self.ring.clear()

    def _set_strip_program(self, program, transient=False):
        # don't allow program change on disconnected devices
        if not self.strip.initialized:
            return

        self.strip_program.release()
        program.use()

        self.last_strip_program = self.strip_program
        self.strip_program = program
        if not transient:
            Setting.objects.filter(key='last_strip_program').update(
                value=self.last_strip_program.name)
            Setting.objects.filter(key='strip_program').update(
                value=self.strip_program.name)
        self.consumers_changed()

        if program.name == 'Disabled':
            self.strip.clear()

    def _set_screen_program(self, program, transient=False):
        if not self.screen.initialized:
            return
        self.screen_program.release()
        program.use()

        self.last_screen_program = self.screen_program
        self.screen_program = program
        if not transient:
            Setting.objects.filter(key='last_screen_program').update(
                value=self.last_screen_program.name)
            Setting.objects.filter(key='screen_program').update(
                value=self.screen_program.name)
        self.consumers_changed()

    def alarm_started(self):
        # make alarm the current program but don't update the database
        with self.option_lock:
            self.alarm_program.use()
            self.last_fixed_color = self.programs['Fixed'].color
            self._set_ring_program(self.programs['Fixed'], transient=True)
            self._set_strip_program(self.programs['Fixed'], transient=True)
            # the screen program adapts with the alarm and is not changed

    def alarm_stopped(self):
        with self.option_lock:
            self.alarm_program.release()
            self.programs['Fixed'].color = self.last_fixed_color
            self._set_ring_program(self.last_ring_program, transient=True)
            self._set_strip_program(self.last_strip_program, transient=True)

            # read last programs from database, which is still in the state before the alarm
            last_ring_program_name = Setting.objects.get(
                key='last_ring_program').value
            last_strip_program_name = Setting.objects.get(
                key='last_strip_program').value
            self.last_ring_program = self.programs[last_ring_program_name]
            self.last_strip_program = self.programs[last_strip_program_name]

    def state_dict(self):
        state_dict = self.base.state_dict()
        state_dict['ring_connected'] = self.ring.initialized
        state_dict['ring_program'] = self.ring_program.name
        state_dict['ring_brightness'] = self.ring.brightness
        state_dict['ring_monochrome'] = self.ring.monochrome
        state_dict['strip_connected'] = self.strip.initialized
        state_dict['strip_program'] = self.strip_program.name
        state_dict['strip_brightness'] = self.strip.brightness
        state_dict['screen_connected'] = self.screen.initialized
        state_dict['screen_program'] = self.screen_program.name
        state_dict['program_speed'] = self.program_speed
        state_dict['fixed_color'] = '#{:02x}{:02x}{:02x}'.format(
            *(int(val * 255) for val in self.programs['Fixed'].color))
        return state_dict

    def get_state(self, request):
        state = self.state_dict()
        return JsonResponse(state)

    def update_state(self):
        state_handler.update_state(self.state_dict())

    def index(self, request):
        context = self.base.context(request)
        # programs that have a strip_color or ring_color function are color programs
        # programs that have a draw function are screen programs
        context['color_program_names'] = [
            program.name for program in self.programs.values()
            if getattr(program, 'ring_colors', None)
        ]
        context['screen_program_names'] = [
            program.name for program in self.programs.values()
            if getattr(program, 'draw', None)
        ]
        #context['program_names'].remove('Alarm')
        return render(request, 'lights.html', context)

    # every option change needs to be synchronized
    # also it changes the state
    def option(func):
        def _decorator(self, request, *args, **kwargs):
            # only privileged users can change options during voting system
            if self.base.settings.voting_system and not self.base.user_manager.has_controls(
                    request.user):
                return HttpResponseForbidden()
            # don't allow option changes during alarm
            if self.base.musiq.player.alarm_playing.is_set():
                return HttpResponseForbidden()
            with self.option_lock:
                try:
                    response = func(self, request, *args, **kwargs)
                    if response is not None:
                        return response
                except (ValueError, IndexError) as e:
                    print('error during lights option: ' + str(e))
                    return HttpResponseBadRequest()
                self.update_state()
            return HttpResponse()

        return wraps(func)(_decorator)

    @option
    def set_lights_shortcut(self, request):
        should_enable = request.POST.get('value') == 'true'
        is_enabled = self.ring_program.name != 'Disabled' or self.strip_program.name != 'Disabled'
        if should_enable == is_enabled:
            return
        if should_enable:
            self._set_ring_program(self.last_ring_program)
            self._set_strip_program(self.last_strip_program)
        else:
            self._set_ring_program(self.programs['Disabled'])
            self._set_strip_program(self.programs['Disabled'])

    @option
    def set_ring_program(self, request):
        program_name = request.POST.get('program')
        program = self.programs[program_name]
        if program == self.ring_program:
            # the program doesn't change, return immediately
            return
        self._set_ring_program(program)

    @option
    def set_ring_brightness(self, request):
        # raises ValueError on wrong input, caught in option decorator
        value = float(request.POST.get('value'))
        self.ring.brightness = value

    @option
    def set_ring_monochrome(self, request):
        enabled = request.POST.get('value') == 'true'
        self.ring.monochrome = enabled

    @option
    def set_strip_program(self, request):
        program_name = request.POST.get('program')
        program = self.programs[program_name]
        if program == self.strip_program:
            # the program doesn't change, return immediately
            return
        self._set_strip_program(program)

    @option
    def set_strip_brightness(self, request):
        # raises ValueError on wrong input, caught in option decorator
        value = float(request.POST.get('value'))
        self.strip.brightness = value

    @option
    def adjust_screen(self, request):
        if self.screen_program.name != 'Disabled':
            return HttpResponseBadRequest(
                'Disable the screen program before readjusting')
        self.screen.adjust()

    @option
    def set_screen_program(self, request):
        program_name = request.POST.get('program')
        program = self.programs[program_name]
        if program == self.screen_program:
            # the program doesn't change, return immediately
            return
        self._set_screen_program(program)

    @option
    def set_program_speed(self, request):
        value = float(request.POST.get('value'))
        self.program_speed = value

    @option
    def set_fixed_color(self, request):
        hex_col = request.POST.get('value').lstrip('#')
        # raises IndexError on wrong input, caught in option decorator
        color = tuple(int(hex_col[i:i + 2], 16) / 255 for i in (0, 2, 4))
        self.programs['Fixed'].color = color