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
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