From c5bc30cef6e25bed9a54b4ea2e1bdc741fec23d8 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sun, 14 Dec 2025 19:18:43 +0100 Subject: [PATCH 01/14] add: towards dimmer brightness mixin --- src/model/filter.py | 1 + src/model/virtual_filters/range_adapters.py | 87 +++++++++++++++++++ src/model/virtual_filters/vfilter_factory.py | 5 +- .../editor/show_browser/fixture_to_filter.py | 1 + 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/model/filter.py b/src/model/filter.py index 696f6874..93bd3533 100644 --- a/src/model/filter.py +++ b/src/model/filter.py @@ -82,6 +82,7 @@ class FilterTypeEnumeration(IntFlag): Negative values indicate virtual filters. """ + VFILTER_DIMMER_BRIGHTNESS_MIXIN = -13 VFILTER_SEQUENCER = -12 VFILTER_COLOR_MIXER = -11 VFILTER_IMPORT = -10 diff --git a/src/model/virtual_filters/range_adapters.py b/src/model/virtual_filters/range_adapters.py index e92793ea..f86d696f 100644 --- a/src/model/virtual_filters/range_adapters.py +++ b/src/model/virtual_filters/range_adapters.py @@ -115,6 +115,93 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: filter_list.append(filter_) +class DimmerGlobalBrightnessMixinVFilter(VirtualFilter): + """V-Filter that allows brightness mixin for 8bit and 16bit values.""" + + def __init__(self, scene: Scene, filter_id: str, pos: tuple[int, int] | None = None) -> None: + """Instantiate a new dimmer brightness mixin vfilter.""" + super().__init__(scene, filter_id, FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN, pos=pos) + self._configuration_supported = True + self._out_data_types["dimmer_out8b"] = DataType.DT_8_BIT + self._out_data_types["dimmer_out16b"] = DataType.DT_16_BIT + self._in_data_types["input"] = DataType.DT_16_BIT # TODO make this configurable + self._in_data_types["mixin"] = DataType.DT_16_BIT + + @override + def resolve_output_port_id(self, virtual_port_id: str) -> str | None: + out_16b = self.filter_configurations.get("has_16bit_output") == "true" + out_8b = self.filter_configurations.get("has_8bit_output") == "true" + if virtual_port_id == "dimmer_out8b": + if out_8b and out_16b: + return f"{self.filter_id}_16b_downsampler:upper" + if out_8b: + return f"{self._filter_id}_8b_range:value" + raise ValueError("Requested 8bit output port but 8bit output is disabled.") + if virtual_port_id == "dimmer_out16b": + if out_16b: + return f"{self._filter_id}_16b_range:value" + raise ValueError("Requested 8bit output port but 16bit output is disabled.") + raise ValueError("Unknown output port") + + @override + def instantiate_filters(self, filter_list: list[Filter]) -> None: + out_16b = self.filter_configurations.get("has_16bit_output") == "true" + out_8b = self.filter_configurations.get("has_8bit_output") == "true" + needs_8bit_input = self.filter_configurations["input_method"] == "8bit" + needs_global_brightness_input = self.channel_links.get("mixin") is None + # TODO place const float 1.0 if mixin port not connected + # TODO add input global brightness dimmer if input port not connected + + # TODO add 8bit to float range adapter if required + # TODO add 16bit to float range adapter if required + + # TODO inst mac filter with offset = 0 + + if out_16b: + range16b_out_filter = Filter( + self.scene, + f"{self._filter_id}_16b_range", + FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_16BIT_RANGE, + pos=self.pos, + filter_configurations={ + "lower_bound_in": "0.0", + "upper_bound_in": "1.0", + "lower_bound_out": "0", + "upper_bound_out": "65565", + "limit_range": "1" + } + ) + # TODO configure input of range16b_out_filter + filter_list.append(range16b_out_filter) + if out_8b: + pass # TODO inst 16bit to 8bit adapter as f"{self.filter_id}_16b_downsampler" + else: + range8b_out_filter = Filter( + self.scene, + f"{self._filter_id}_8b_range", + FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_8BIT_RANGE, + pos=self.pos, + filter_configurations={ + "lower_bound_in": "0.0", + "upper_bound_in": "1.0", + "lower_bound_out": "0", + "upper_bound_out": "255", + "limit_range": "1" + } + ) + # TODO configure input of range8b_out_filter + filter_list.append(range8b_out_filter) + + @override + def deserialize(self) -> None: + if self.filter_configurations.get("has_8bit_output") is None: + self.filter_configurations["has_8bit_output"] = "true" + if self.filter_configurations.get("has_16bit_output") is None: + self.filter_configurations["has_16bit_output"] = "false" + if self.filter_configurations.get("input_method") is None: + self.filter_configurations["input_method"] = "16bit" + + class ColorGlobalBrightnessMixinVFilter(VirtualFilter): """V-Filter that provides the global brightness property.""" diff --git a/src/model/virtual_filters/vfilter_factory.py b/src/model/virtual_filters/vfilter_factory.py index b8494eb0..af7da8eb 100644 --- a/src/model/virtual_filters/vfilter_factory.py +++ b/src/model/virtual_filters/vfilter_factory.py @@ -17,6 +17,7 @@ from model.virtual_filters.pan_tilt_constant import PanTiltConstantFilter from model.virtual_filters.range_adapters import ( ColorGlobalBrightnessMixinVFilter, + DimmerGlobalBrightnessMixinVFilter, EightBitToFloatRange, SixteenBitToFloatRange, ) @@ -51,11 +52,11 @@ def construct_virtual_filter_instance( return None case FilterTypeEnumeration.VFILTER_POSITION_CONSTANT: return PanTiltConstantFilter(scene, filter_id, pos=pos) - + case FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN: + return DimmerGlobalBrightnessMixinVFilter(scene, filter_id, pos=pos) case FilterTypeEnumeration.VFILTER_CUES: return CueFilter(scene, filter_id, pos=pos) case FilterTypeEnumeration.VFILTER_EFFECTSSTACK: - # TODO implement effects stack virtual filter (as described in issue #87) return EffectsStack(scene, filter_id, pos=pos) case FilterTypeEnumeration.VFILTER_AUTOTRACKER: return AutoTrackerFilter(scene, filter_id, pos=pos) diff --git a/src/view/show_mode/editor/show_browser/fixture_to_filter.py b/src/view/show_mode/editor/show_browser/fixture_to_filter.py index e2564b97..a78cdcf9 100644 --- a/src/view/show_mode/editor/show_browser/fixture_to_filter.py +++ b/src/view/show_mode/editor/show_browser/fixture_to_filter.py @@ -165,6 +165,7 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co # output_map[c[c_i]] = adapter_name + ":value" #TODO i += 1 elif channel.name == "Dimmer": + # TODO replace with brightness mixin vfilter dimmer_name = _sanitize_name(f"dimmer_{i}_{name}") global_dimmer_filter = Filter(scene=fp.parent_scene, filter_id=dimmer_name, From ce652a8a9e6ebd2b97608618b17380bc090219d6 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Mon, 15 Dec 2025 19:02:48 +0100 Subject: [PATCH 02/14] add: finished implementation of filter inst --- src/model/virtual_filters/range_adapters.py | 147 ++++++++++++++++++-- 1 file changed, 135 insertions(+), 12 deletions(-) diff --git a/src/model/virtual_filters/range_adapters.py b/src/model/virtual_filters/range_adapters.py index f86d696f..1ae788c6 100644 --- a/src/model/virtual_filters/range_adapters.py +++ b/src/model/virtual_filters/range_adapters.py @@ -116,16 +116,28 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: class DimmerGlobalBrightnessMixinVFilter(VirtualFilter): - """V-Filter that allows brightness mixin for 8bit and 16bit values.""" + """V-Filter that allows brightness mixin for 8bit and 16bit values. + + The filter allows the configuration of an input and a mixin input channel. + Their defaults are the global brightness and a constant 1. + If they're connected their input data typed can both be configured as either 8bit or 16bit. + The optional offset input channel needs to be a float. Reasonable values range from (-1, 1). + + The outputs can be configured as 8bit or 16bit. + """ def __init__(self, scene: Scene, filter_id: str, pos: tuple[int, int] | None = None) -> None: """Instantiate a new dimmer brightness mixin vfilter.""" super().__init__(scene, filter_id, FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN, pos=pos) self._configuration_supported = True + self.filter_configurations.setdefault("has_16bit_output", "true") + self.filter_configurations.setdefault("has_8bit_output", "true") + self.filter_configurations.setdefault("input_method", "8bit") + self.filter_configurations.setdefault("input_method_mixin", "8bit") self._out_data_types["dimmer_out8b"] = DataType.DT_8_BIT self._out_data_types["dimmer_out16b"] = DataType.DT_16_BIT - self._in_data_types["input"] = DataType.DT_16_BIT # TODO make this configurable - self._in_data_types["mixin"] = DataType.DT_16_BIT + self._in_data_types["offset"] = DataType.DT_DOUBLE + self.deserialize() @override def resolve_output_port_id(self, virtual_port_id: str) -> str | None: @@ -148,14 +160,76 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: out_16b = self.filter_configurations.get("has_16bit_output") == "true" out_8b = self.filter_configurations.get("has_8bit_output") == "true" needs_8bit_input = self.filter_configurations["input_method"] == "8bit" - needs_global_brightness_input = self.channel_links.get("mixin") is None - # TODO place const float 1.0 if mixin port not connected - # TODO add input global brightness dimmer if input port not connected + required_mixin_input_method = 1 if self.filter_configurations["input_method_mixin"] == "8bit" else 2 + needs_global_brightness_input = self.channel_links.get("input") is None + needs_const_mixin = self.channel_links.get("mixin") is None + needs_offset = self.channel_links.get("offset") is None - # TODO add 8bit to float range adapter if required - # TODO add 16bit to float range adapter if required + if needs_const_mixin and (not needs_global_brightness_input and not needs_offset): + const_mixin_filter = Filter( + self.scene, + f"{self.filter_id}_const_mixin", + FilterTypeEnumeration.FILTER_CONSTANT_FLOAT, + pos=self.pos + ) + const_mixin_filter.initial_parameters["value"] = "1.0" + filter_list.append(const_mixin_filter) + mixin_port_name = f"{self.filter_id}_const_mixin:value" + required_mixin_input_method = 0 + else: + mixin_port_name = self.channel_links.get("mixin") - # TODO inst mac filter with offset = 0 + if needs_global_brightness_input: + global_brightness_filter = Filter( + self.scene, + f"{self.filter_id}_global_brightness_input", + FilterTypeEnumeration.FILTER_TYPE_MAIN_BRIGHTNESS, + pos=self.pos + ) + filter_list.append(global_brightness_filter) + input_port_name = f"{self.filter_id}_global_brightness_input:brightness" + needs_8bit_input = False + else: + input_port_name = self.channel_links.get("input") + + if needs_8bit_input: + range_8b_to_float_filter = self._generate_8b_to_float_range(filter_list, input_port_name) + input_port_name = range_8b_to_float_filter.resolve_output_port_id("value") + else: + range_16b_to_float_filter = self._generate_16b_to_float_range(filter_list, input_port_name) + input_port_name = range_16b_to_float_filter.resolve_output_port_id("value") + + if required_mixin_input_method == 1: + range_8b_to_float_filter = self._generate_8b_to_float_range(filter_list, mixin_port_name) + mixin_port_name = range_8b_to_float_filter.resolve_output_port_id("value") + elif required_mixin_input_method == 2: + range_16b_to_float_filter = self._generate_16b_to_float_range(filter_list, mixin_port_name) + mixin_port_name = range_16b_to_float_filter.resolve_output_port_id("value") + + if not (needs_global_brightness_input and needs_const_mixin and needs_offset): + if needs_offset: + offset_filter = Filter( + self.scene, + f"{self.filter_id}_offset", + FilterTypeEnumeration.FILTER_CONSTANT_FLOAT, + pos=self.pos + ) + offset_filter.initial_parameters["value"] = "0.0" + filter_list.append(offset_filter) + offset_input_port = offset_filter.filter_id + ":value" + else: + offset_input_port = self.channel_links.get("offset") + mac_filter = Filter( + self.scene, + f"{self.filter_id}_mac", + FilterTypeEnumeration.FILTER_ARITHMETICS_MAC, + pos=self.pos + ) + mac_filter.channel_links["factor1"] = input_port_name + mac_filter.channel_links["factor2"] = mixin_port_name + mac_filter.channel_links["summand"] = offset_input_port + filter_list.append(mac_filter) + input_port_name = mac_filter.filter_id + ":value" if out_16b: range16b_out_filter = Filter( @@ -171,10 +245,17 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: "limit_range": "1" } ) - # TODO configure input of range16b_out_filter + range16b_out_filter.channel_links["value_in"] = input_port_name filter_list.append(range16b_out_filter) if out_8b: - pass # TODO inst 16bit to 8bit adapter as f"{self.filter_id}_16b_downsampler" + downsampling_filter = Filter( + self.scene, + f"{self._filter_id}_16b_downsampler", + FilterTypeEnumeration.FILTER_ADAPTER_16BIT_TO_DUAL_8BIT, + pos=self.pos + ) + downsampling_filter.channel_links["value"] = f"{range16b_out_filter.filter_id}:value" + filter_list.append(downsampling_filter) else: range8b_out_filter = Filter( self.scene, @@ -189,9 +270,43 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: "limit_range": "1" } ) - # TODO configure input of range8b_out_filter + range8b_out_filter.channel_links["value_in"] = input_port_name filter_list.append(range8b_out_filter) + def _generate_16b_to_float_range(self, filter_list: list[Filter], input_port_name: str) -> SixteenBitToFloatRange: + range_16b_to_float_filter = SixteenBitToFloatRange( + self.scene, + f"{self.filter_id}_16bit_to_float", + pos=self.pos + ) + range_16b_to_float_filter.filter_configurations.update({ + "lower_bound_in": "0", + "upper_bound_in": "65565", + "lower_bound_out": "0.0", + "upper_bound_out": "1.0", + "limit_range": "1" + }) + range_16b_to_float_filter.channel_links["value_in"] = input_port_name + range_16b_to_float_filter.instantiate_filters(filter_list) + return range_16b_to_float_filter + + def _generate_8b_to_float_range(self, filter_list: list[Filter], input_port_name: str) -> EightBitToFloatRange: + range_8b_to_float_filter = EightBitToFloatRange( + self.scene, + f"{self.filter_id}_8bit_to_float", + pos=self.pos + ) + range_8b_to_float_filter.filter_configurations.update({ + "lower_bound_in": "0", + "upper_bound_in": "255", + "lower_bound_out": "0.0", + "upper_bound_out": "1.0", + "limit_range": "1" + }) + range_8b_to_float_filter.channel_links["value_in"] = input_port_name + range_8b_to_float_filter.instantiate_filters(filter_list) + return range_8b_to_float_filter + @override def deserialize(self) -> None: if self.filter_configurations.get("has_8bit_output") is None: @@ -200,6 +315,14 @@ def deserialize(self) -> None: self.filter_configurations["has_16bit_output"] = "false" if self.filter_configurations.get("input_method") is None: self.filter_configurations["input_method"] = "16bit" + if self.filter_configurations.get("input_method") == "8bit": + self._in_data_types["input"] = DataType.DT_8_BIT + else: + self._in_data_types["input"] = DataType.DT_16_BIT + if self.filter_configurations.get("input_method_mixin") == "8bit": + self._in_data_types["mixin"] = DataType.DT_8_BIT + else: + self._in_data_types["mixin"] = DataType.DT_16_BIT class ColorGlobalBrightnessMixinVFilter(VirtualFilter): From 6d5f57e78403728c4a63d627cb6ac27a66d6cb39 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Tue, 16 Dec 2025 11:33:11 +0100 Subject: [PATCH 03/14] add: node implementation of dimmer brightness mixin --- .../editor/nodes/filter_node_library.py | 2 ++ .../show_mode/editor/nodes/impl/adapters.py | 33 ++++++++++++++++++- .../editor/nodes/type_to_node_dict.py | 2 ++ .../editor/show_browser/fixture_to_filter.py | 20 +++++++++-- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/view/show_mode/editor/nodes/filter_node_library.py b/src/view/show_mode/editor/nodes/filter_node_library.py index a9f97cec..1b8e711d 100644 --- a/src/view/show_mode/editor/nodes/filter_node_library.py +++ b/src/view/show_mode/editor/nodes/filter_node_library.py @@ -19,6 +19,7 @@ AdapterFloatToRange, ColorBrightnessMixinNode, CombineTwo8BitToSingle16Bit, + DimmerBrightnessMixinNode, Map8BitTo16Bit, ) from view.show_mode.editor.nodes.impl.arithmetics import ( @@ -159,6 +160,7 @@ def _register_adapters_nodes(self) -> None: self.addNodeType(CombineTwo8BitToSingle16Bit, [("Adapters",)]) self.addNodeType(Map8BitTo16Bit, [("Adapters",)]) self.addNodeType(ColorBrightnessMixinNode, [("Adapters",)]) + self.addNodeType(DimmerBrightnessMixinNode, [("Adapters",)]) def _register_arithmetic_nodes(self) -> None: """Register all the arithmetics nodes.""" diff --git a/src/view/show_mode/editor/nodes/impl/adapters.py b/src/view/show_mode/editor/nodes/impl/adapters.py index 4d693530..958410ee 100644 --- a/src/view/show_mode/editor/nodes/impl/adapters.py +++ b/src/view/show_mode/editor/nodes/impl/adapters.py @@ -1,6 +1,8 @@ """Adapters and converters filter nodes""" +from typing import override + from model import DataType, Scene -from model.filter import Filter, FilterTypeEnumeration +from model.filter import Filter, FilterTypeEnumeration, VirtualFilter from view.show_mode.editor.nodes.base.filternode import FilterNode @@ -316,3 +318,32 @@ def __init__(self, model: Filter | Scene, name: str) -> None: self.filter.in_data_types["brightness"] = DataType.DT_8_BIT self.channel_hints["brightness"] = "[0-255, optional]" self.filter._configuration_supported = False + + +class DimmerBrightnessMixinNode(FilterNode): + nodeName = "Dimmer Brightness Mixin" # noqa: N815 + + def __init__(self, model: Filter | Scene, name: str) -> None: + super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN, name=name, + terminals={"input": {"io": "in"}, "mixin": {"io": "in"}, "offset": {"io": "in"}}) + self.channel_hints["offset"] = "[(-1, 1), optional]" + self.channel_hints["input"] = "[default: global brightness]" + self.channel_hints["mixin"] = "[optional]" + self._update_output_terminals() + + def _update_output_terminals(self) -> None: + for setting, term_name in [("has_8bit_output", "dimmer_out8b"), ("has_16bit_output", "dimmer_out16b")]: + if self.filter.filter_configurations.get(setting) == "true": + if self.outputs().get(term_name) is None: + self.addOutput(term_name) + else: + if self.outputs().get(term_name) is not None: + self.removeTerminal(term_name) + + @override + def update_node_after_settings_changed(self) -> None: + if isinstance(self.filter, VirtualFilter): + self.filter.deserialize() + else: + raise ValueError("Expected filter instance to be a DimmerGlobalBrightnessMixinVFilter, implying a vFilter.") + self._update_output_terminals() diff --git a/src/view/show_mode/editor/nodes/type_to_node_dict.py b/src/view/show_mode/editor/nodes/type_to_node_dict.py index 5114ac21..4a83fc4b 100644 --- a/src/view/show_mode/editor/nodes/type_to_node_dict.py +++ b/src/view/show_mode/editor/nodes/type_to_node_dict.py @@ -18,6 +18,7 @@ AdapterFloatToRange, ColorBrightnessMixinNode, CombineTwo8BitToSingle16Bit, + DimmerBrightnessMixinNode, Map8BitTo16Bit, ) from view.show_mode.editor.nodes.impl.arithmetics import ( @@ -101,6 +102,7 @@ FilterTypeEnumeration.VFILTER_SEQUENCER: SequencerNode.nodeName, FilterTypeEnumeration.VFILTER_COLOR_MIXER: ColorMixerVFilterNode.nodeName, FilterTypeEnumeration.VFILTER_IMPORT: ImportNode.nodeName, + FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN: DimmerBrightnessMixinNode.nodeName, FilterTypeEnumeration.VFILTER_COLOR_GLOBAL_BRIGHTNESS_MIXIN: ColorBrightnessMixinNode.nodeName, FilterTypeEnumeration.VFILTER_POSITION_CONSTANT: PanTiltConstant.nodeName, FilterTypeEnumeration.VFILTER_CUES: CueListNode.nodeName, diff --git a/src/view/show_mode/editor/show_browser/fixture_to_filter.py b/src/view/show_mode/editor/show_browser/fixture_to_filter.py index a78cdcf9..901dafdb 100644 --- a/src/view/show_mode/editor/show_browser/fixture_to_filter.py +++ b/src/view/show_mode/editor/show_browser/fixture_to_filter.py @@ -1,4 +1,4 @@ -"""write fixture to file""" +"""write fixture to file.""" from logging import getLogger from typing import Union @@ -26,6 +26,20 @@ def _sanitize_name(input_: str | dict) -> str: def place_fixture_filters_in_scene(fixture: UsedFixture | tuple[UsedFixture, ColorSupport], filter_page: FilterPage, output_map: dict[Union[ ColorSupport, str], str] | None = None) -> bool: + """Generate fixture control filters from a given fixture. + + Purpose of the output map: A fixture has certain features (such as color segments). These features receive special + filters to drive them. Their input ports are placed in this map in order to enable higher level automated tools + (such as the effects stack system) to find them and connect to them. + + Args: + fixture: The fixture to generate filters from. + filter_page: The page to place the new filters in. + output_map: A map that should receive the mapped fixture features. + + Returns: + True if the operation was successful. + """ # TODO output_map do nothing if isinstance(fixture, tuple): fixture = fixture[0] @@ -116,7 +130,7 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co _sanitize_name(channel.name)] = adapter_name + ":value_upper" fp.filters.append(split_filter) # if output_map is not None: - # output_map[c[c_i]] = split_filter.filter_id + ":value" #TODO + # output_map[c[c_i]] = split_filter.filter_id + ":value" #FIXME already_added_filters.append(split_filter) i += 1 elif channel.name.startswith("Red"): @@ -162,7 +176,7 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co fp.filters.append(rgb_filter) already_added_filters.append(rgb_filter) # if output_map is not None: - # output_map[c[c_i]] = adapter_name + ":value" #TODO + # output_map[c[c_i]] = adapter_name + ":value" # FIXME i += 1 elif channel.name == "Dimmer": # TODO replace with brightness mixin vfilter From b5db3afb470b46c4b8816eebccdb4289c6dd94fd Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Tue, 16 Dec 2025 13:40:12 +0100 Subject: [PATCH 04/14] fix: documentation in adapter nodes file --- .../show_mode/editor/nodes/impl/adapters.py | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/view/show_mode/editor/nodes/impl/adapters.py b/src/view/show_mode/editor/nodes/impl/adapters.py index 958410ee..740e43df 100644 --- a/src/view/show_mode/editor/nodes/impl/adapters.py +++ b/src/view/show_mode/editor/nodes/impl/adapters.py @@ -1,4 +1,4 @@ -"""Adapters and converters filter nodes""" +"""Adapters and converters filter nodes.""" from typing import override from model import DataType, Scene @@ -8,9 +8,11 @@ class Adapter16BitTo8BitNode(FilterNode): """Filter to convert a 16 bit value to two 8 bit values.""" + nodeName = "16 bit to 8 bit converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize 16bit to 8bit splitter adapter node.""" super().__init__(model=model, filter_type=int(FilterTypeEnumeration.FILTER_ADAPTER_16BIT_TO_DUAL_8BIT), name=name, terminals={ "value": {"io": "in"}, @@ -27,9 +29,11 @@ class Adapter16BitToBoolNode(FilterNode): """Filter to convert a 16 bit value to a boolean. If input is 0, output is 0, else 1. """ + nodeName = "16 bit to bool converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize 16bit to boolean adapter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_16BIT_TO_BOOL, name=name, terminals={ "value_in": {"io": "in"}, @@ -41,9 +45,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class Adapter16bitToFloat(FilterNode): + """Node for 16bit to float adapter filter.""" + nodeName = "16bit to Float converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize 16bit to float adapter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_TYPE_ADAPTER_16BIT_TO_FLOAT, name=name, terminals={ "value_in": {"io": "in"}, @@ -56,9 +63,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class Adapter8bitToFloat(FilterNode): + """Node for 8bit to float adapter filter.""" + nodeName = "8bit to Float converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize 8bit to float adapter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_TYPE_ADAPTER_8BIT_TO_FLOAT, name=name, terminals={ "value_in": {"io": "in"}, @@ -72,9 +82,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterColorToRGBNode(FilterNode): """Filter to convert a color value to a rgb value.""" + nodeName = "Color to rgb converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Color to rgb adapter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_COLOR_TO_RGB, name=name, terminals={ "value": {"io": "in"}, @@ -91,9 +103,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterColorToRGBWNode(FilterNode): """Filter to convert a color value to a rgbw value.""" + nodeName = "Color to rgb-w converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Color to rgb-w adapter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_COLOR_TO_RGBW, name=name, terminals={ "value": {"io": "in"}, @@ -112,9 +126,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterColorToRGBWANode(FilterNode): """Filter to convert a color value to a RGBWA value.""" + nodeName = "Color to rgb-wa converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Color to rgb-wa adapter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_COLOR_TO_RGBWA, name=name, terminals={ "value": {"io": "in"}, @@ -135,9 +151,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterFloatToColorNode(FilterNode): """Filter to convert a float/double value to a color value.""" + nodeName = "Float to color converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Float to color combining converter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_COLOR, name=name, terminals={ "h": {"io": "in"}, @@ -155,9 +173,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterColorToFloatsNode(FilterNode): """Filter that splits the HSI values into three individual float channels.""" + nodeName = "Color to Float converter" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Color to Float converter node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_COLOR_TO_FLOAT, name=name, terminals={ "input": {"io": "in"}, @@ -173,13 +193,14 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterFloatToRange(FilterNode): - """Filter maps a range of floats to another range of specific type (template)""" + """Filter maps a range of floats to another range of specific type (template).""" nodeName = "float range to float range" # noqa: N815 def __init__(self, model: Filter | Scene, name: str, filter_type: FilterTypeEnumeration = FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_FLOAT_RANGE) -> None: + """Initialize float range to float range template node.""" super().__init__(model, int(filter_type), name, terminals={ "value_in": {"io": "in"}, "value": {"io": "out"}, @@ -215,10 +236,12 @@ def __init__(self, model: Filter | Scene, name: str, class AdapterFloatTo8BitRange(AdapterFloatToRange): - """Filter maps a range of float to a range of 8bit""" + """Filter maps a range of float to a range of 8bit.""" + nodeName = "Float range to 8bit" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize float range to 8bit range node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_8BIT_RANGE, name=name) try: self.filter.initial_parameters["upper_bound_out"] = model.initial_parameters["upper_bound_out"] @@ -228,10 +251,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class AdapterFloatTo16BitRange(AdapterFloatToRange): - """Filter maps a range of float to a range of 16bit""" + """Filter maps a range of float to a range of 16bit.""" + nodeName = "Float range to 16bit" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize float range to 16bit range node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_16BIT_RANGE, name=name) try: self.filter.initial_parameters["upper_bound_out"] = model.initial_parameters["upper_bound_out"] @@ -241,10 +266,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class Adapter16BitToRangeFloat(AdapterFloatToRange): - """Filter maps a range of 16bit to a range of float""" + """Filter maps a range of 16bit to a range of float.""" + nodeName = "16bit range to Float" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize 16bit to float range node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FILTER_ADAPTER_16BIT_TO_FLOAT_RANGE, name=name) try: @@ -256,10 +283,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class Adapter8BitToRangeFloat(AdapterFloatToRange): - """Filter maps a range of 8bit to a range of floats""" + """Filter maps a range of 8bit to a range of floats.""" + nodeName = "8bit range to Float" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize 8bit to float range node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FILTER_ADAPTER_8BIT_TO_FLOAT_RANGE, name=name) try: @@ -272,9 +301,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class CombineTwo8BitToSingle16Bit(FilterNode): """Filter that combines two 8bit values to a 16bit one.""" + nodeName = "Dual 8bit to single 16bit" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Dual 8bit to single 16bit combiner node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_DUAL_BYTE_TO_16BIT, name=name, terminals={ "lower": {"io": "in"}, @@ -289,9 +320,11 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class Map8BitTo16Bit(FilterNode): """Filter that maps an 8-Bit value to a 16-Bit one.""" + nodeName = "Map 8bit to 16bit" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Map 8bit to 16bit map node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_ADAPTER_8BIT_TO_16BIT, name=name, terminals={ "value_in": {"io": "in"}, @@ -303,9 +336,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class ColorBrightnessMixinNode(FilterNode): + """Node for color brightness mixin v-filter.""" + nodeName = "Color Brightness Mixin" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Color Brightness Mixin node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_COLOR_GLOBAL_BRIGHTNESS_MIXIN, name=name, terminals={ "out": {"io": "out"}, @@ -321,9 +357,12 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class DimmerBrightnessMixinNode(FilterNode): + """Node for dimmer brightness mixin v-filter.""" + nodeName = "Dimmer Brightness Mixin" # noqa: N815 def __init__(self, model: Filter | Scene, name: str) -> None: + """Initialize Dimmer Brightness Mixin node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN, name=name, terminals={"input": {"io": "in"}, "mixin": {"io": "in"}, "offset": {"io": "in"}}) self.channel_hints["offset"] = "[(-1, 1), optional]" From 8e1bc1700a60047fb53b0fb605c95c5ebe6e5883 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Tue, 16 Dec 2025 14:21:55 +0100 Subject: [PATCH 05/14] add: config widget --- .../show_mode/editor/filter_settings_item.py | 4 +- .../dimmer_brightness_mixin_config_widget.py | 88 +++++++++++++++++++ .../show_mode/editor/nodes/impl/adapters.py | 1 + .../editor/show_browser/fixture_to_filter.py | 1 + 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py diff --git a/src/view/show_mode/editor/filter_settings_item.py b/src/view/show_mode/editor/filter_settings_item.py index 916bb305..e4d7f02f 100644 --- a/src/view/show_mode/editor/filter_settings_item.py +++ b/src/view/show_mode/editor/filter_settings_item.py @@ -29,6 +29,7 @@ from .node_editor_widgets.autotracker_settings import AutotrackerSettingsWidget from .node_editor_widgets.color_mixing_setup_widget import ColorMixingSetupWidget from .node_editor_widgets.column_select import ColumnSelect +from .node_editor_widgets.dimmer_brightness_mixin_config_widget import DimmerBrightnessMixinConfigWidget from .node_editor_widgets.import_vfilter_settings_widget import ImportVFilterSettingsWidget from .node_editor_widgets.lua_widget import LuaScriptConfigWidget from .node_editor_widgets.sequencer_editor.widget import SequencerEditor @@ -133,7 +134,8 @@ def check_if_filter_has_special_widget(filter_: Filter) -> NodeEditorFilterConfi return ColorMixingSetupWidget() if filter_.filter_type == FilterTypeEnumeration.VFILTER_SEQUENCER: return SequencerEditor(f=filter_) - + if filter_.filter_type == FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN: + return DimmerBrightnessMixinConfigWidget() return None diff --git a/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py b/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py new file mode 100644 index 00000000..00ca8228 --- /dev/null +++ b/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py @@ -0,0 +1,88 @@ +from typing import override + +from PySide6.QtWidgets import QWidget, QFormLayout, QCheckBox, QButtonGroup, QRadioButton, QHBoxLayout, QLabel + +from view.show_mode.editor.node_editor_widgets import NodeEditorFilterConfigWidget + + +class DimmerBrightnessMixinConfigWidget(NodeEditorFilterConfigWidget): + """Configuration widget for dimmer brightness mixin node.""" + + def __init__(self, parent: QWidget | None = None) -> None: + """Load the filter and prepare a widget.""" + super().__init__() + self._widget = QWidget(parent=parent) + layout = QFormLayout() + self._cb_has_16bit = QCheckBox(self._widget) + self._cb_has_16bit.setText("Enable 16bit output") + self._cb_has_16bit.checkStateChanged.connect(self._update_warning_visibility) + layout.addWidget(self._cb_has_16bit) + self._cb_has_8bit = QCheckBox(self._widget) + self._cb_has_8bit.setText("Enable 8bit output") + self._cb_has_8bit.checkStateChanged.connect(self._update_warning_visibility) + layout.addWidget(self._cb_has_8bit) + self._output_warning_label = QLabel("At least one output should be configured.", self._widget) + self._output_warning_label.setVisible(False) + self._output_warning_label.setStyleSheet("color: red;") + layout.addWidget(self._output_warning_label) + + self._input_method_rb_group = QButtonGroup(self._widget) + self._input_8bit = QRadioButton("8bit", self._widget) + self._input_method_rb_group.addButton(self._input_8bit) + self._input_16bit = QRadioButton("16bit", self._widget) + self._input_method_rb_group.addButton(self._input_16bit) + button_layout = QHBoxLayout() + button_layout.addWidget(self._input_8bit) + button_layout.addWidget(self._input_16bit) + layout.addRow("Input Port Data Type:", button_layout) + + self._mixin_method_rb_group = QButtonGroup(self._widget) + self._mixin_8bit = QRadioButton("8bit", self._widget) + self._mixin_method_rb_group.addButton(self._mixin_8bit) + self._mixin_16bit = QRadioButton("16bit", self._widget) + self._mixin_method_rb_group.addButton(self._mixin_16bit) + button_layout = QHBoxLayout() + button_layout.addWidget(self._mixin_8bit) + button_layout.addWidget(self._mixin_16bit) + layout.addRow("Mixin Port Data Type:", button_layout) + + self._widget.setLayout(layout) + + def _update_warning_visibility(self): + self._output_warning_label.setVisible(not self._cb_has_8bit.isChecked() and not self._cb_has_16bit.isChecked()) + + @override + def _get_configuration(self) -> dict[str, str]: + return { + "has_16bit_output": "true" if self._cb_has_16bit.isChecked() else "false", + "has_8bit_output": "true" if self._cb_has_8bit.isChecked() else "false", + "input_method": "8bit" if self._input_8bit.isChecked() else "16bit", + "input_method_mixin": "8bit" if self._mixin_8bit.isChecked() else "16bit", + } + + @override + def _load_configuration(self, conf: dict[str, str]) -> None: + self._cb_has_16bit.setChecked(conf.get("has_16bit_output", "false") == "true") + self._cb_has_8bit.setChecked(conf.get("has_8bit_output", "false") == "true") + self._input_8bit.setChecked(conf.get("input_method", "8bit") == "8bit") + self._input_16bit.setChecked(conf.get("input_method", "8bit") == "16bit") + self._mixin_8bit.setChecked(conf.get("input_method_mixin", "8bit") == "8bit") + self._mixin_16bit.setChecked(conf.get("input_method_mixin", "8bit") == "16bit") + + @override + def get_widget(self) -> QWidget: + return self._widget + + @override + def _load_parameters(self, parameters: dict[str, str]) -> dict: + # Nothing to do here + pass + + @override + def _get_parameters(self) -> dict[str, str]: + return {} + + @override + def parent_opened(self) -> None: + # Nothing to do here + pass \ No newline at end of file diff --git a/src/view/show_mode/editor/nodes/impl/adapters.py b/src/view/show_mode/editor/nodes/impl/adapters.py index 740e43df..e7ec8fab 100644 --- a/src/view/show_mode/editor/nodes/impl/adapters.py +++ b/src/view/show_mode/editor/nodes/impl/adapters.py @@ -27,6 +27,7 @@ def __init__(self, model: Filter | Scene, name: str) -> None: class Adapter16BitToBoolNode(FilterNode): """Filter to convert a 16 bit value to a boolean. + If input is 0, output is 0, else 1. """ diff --git a/src/view/show_mode/editor/show_browser/fixture_to_filter.py b/src/view/show_mode/editor/show_browser/fixture_to_filter.py index 901dafdb..524fffb6 100644 --- a/src/view/show_mode/editor/show_browser/fixture_to_filter.py +++ b/src/view/show_mode/editor/show_browser/fixture_to_filter.py @@ -39,6 +39,7 @@ def place_fixture_filters_in_scene(fixture: UsedFixture | tuple[UsedFixture, Col Returns: True if the operation was successful. + """ # TODO output_map do nothing if isinstance(fixture, tuple): From f79752ba34fb2894cad0386d6d6660c0cb05f0d8 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Tue, 16 Dec 2025 14:26:27 +0100 Subject: [PATCH 06/14] fix: missing typo annotation --- .../dimmer_brightness_mixin_config_widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py b/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py index 00ca8228..0d100326 100644 --- a/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py +++ b/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py @@ -1,6 +1,6 @@ from typing import override -from PySide6.QtWidgets import QWidget, QFormLayout, QCheckBox, QButtonGroup, QRadioButton, QHBoxLayout, QLabel +from PySide6.QtWidgets import QButtonGroup, QCheckBox, QFormLayout, QHBoxLayout, QLabel, QRadioButton, QWidget from view.show_mode.editor.node_editor_widgets import NodeEditorFilterConfigWidget @@ -48,7 +48,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self._widget.setLayout(layout) - def _update_warning_visibility(self): + def _update_warning_visibility(self) -> None: self._output_warning_label.setVisible(not self._cb_has_8bit.isChecked() and not self._cb_has_16bit.isChecked()) @override @@ -85,4 +85,4 @@ def _get_parameters(self) -> dict[str, str]: @override def parent_opened(self) -> None: # Nothing to do here - pass \ No newline at end of file + pass From 3907036b4fbb4069323688dbe4f59e878a8f3a66 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Tue, 16 Dec 2025 18:55:28 +0100 Subject: [PATCH 07/14] chg: global dimmer in fixture inst to vfilter --- .../editor/show_browser/fixture_to_filter.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/view/show_mode/editor/show_browser/fixture_to_filter.py b/src/view/show_mode/editor/show_browser/fixture_to_filter.py index 524fffb6..5750ff24 100644 --- a/src/view/show_mode/editor/show_browser/fixture_to_filter.py +++ b/src/view/show_mode/editor/show_browser/fixture_to_filter.py @@ -6,6 +6,7 @@ from model.filter import FilterTypeEnumeration from model.ofl.fixture import ColorSupport, UsedFixture from model.scene import FilterPage +from model.virtual_filters.range_adapters import DimmerGlobalBrightnessMixinVFilter from model.virtual_filters.vfilter_factory import construct_virtual_filter_instance logger = getLogger(__name__) @@ -179,14 +180,15 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co # if output_map is not None: # output_map[c[c_i]] = adapter_name + ":value" # FIXME i += 1 - elif channel.name == "Dimmer": - # TODO replace with brightness mixin vfilter + elif channel.name == "Dimmer" or channel.name == "Intensity": dimmer_name = _sanitize_name(f"dimmer_{i}_{name}") - global_dimmer_filter = Filter(scene=fp.parent_scene, + global_dimmer_filter = DimmerGlobalBrightnessMixinVFilter(scene=fp.parent_scene, filter_id=dimmer_name, - filter_type=49, - pos=(x - 2 * _additional_filter_depth, - compute_filter_height(channel_count, i))) + pos=(int(x - 2 * _additional_filter_depth), + int(compute_filter_height(channel_count, i)))) + global_dimmer_filter.filter_configurations["has_16bit_output"] = "true" + global_dimmer_filter.filter_configurations["has_8bit_output"] = "false" + global_dimmer_filter.deserialize() added_depth = max(added_depth, 2 * _additional_filter_depth) global_dimmer_found = True fp.filters.append(global_dimmer_filter) @@ -194,6 +196,9 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co already_added_filters.append(global_dimmer_filter) dimmer_name = global_dimmer_filter.filter_id x += 10 + + # TODO if we only have a single dimmer port we can output 8bit directly, reducing overhead; this + # requires having a dimmer feature in the fixture definition adapter_name = _sanitize_name(f"dimmer2byte_{i}_{name}") dimmer_to_byte_filter = Filter(scene=fp.parent_scene, filter_id=adapter_name, @@ -203,7 +208,7 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co fp.parent_scene.append_filter(dimmer_to_byte_filter) already_added_filters.append(dimmer_to_byte_filter) adapter_name = dimmer_to_byte_filter.filter_id - dimmer_to_byte_filter.channel_links["value"] = dimmer_name + ":brightness" + dimmer_to_byte_filter.channel_links["value"] = dimmer_name + ":dimmer_out16b" universe_filter.channel_links[_sanitize_name(channel.name)] = adapter_name + ":value_upper" fp.filters.append(dimmer_to_byte_filter) i += 1 From 7650ba3de957c15cce4ea3b9ef252b5d8c806dee Mon Sep 17 00:00:00 2001 From: Doralitze Date: Tue, 16 Dec 2025 20:08:07 +0100 Subject: [PATCH 08/14] add: support for fine dimmer channel --- .../dimmer_brightness_mixin_config_widget.py | 2 + .../editor/show_browser/fixture_to_filter.py | 49 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py b/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py index 0d100326..8ef61aaf 100644 --- a/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py +++ b/src/view/show_mode/editor/node_editor_widgets/dimmer_brightness_mixin_config_widget.py @@ -1,3 +1,5 @@ +"""Module contains dimmer brightness mixin node config widget.""" + from typing import override from PySide6.QtWidgets import QButtonGroup, QCheckBox, QFormLayout, QHBoxLayout, QLabel, QRadioButton, QWidget diff --git a/src/view/show_mode/editor/show_browser/fixture_to_filter.py b/src/view/show_mode/editor/show_browser/fixture_to_filter.py index 5750ff24..5a2601b8 100644 --- a/src/view/show_mode/editor/show_browser/fixture_to_filter.py +++ b/src/view/show_mode/editor/show_browser/fixture_to_filter.py @@ -180,14 +180,21 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co # if output_map is not None: # output_map[c[c_i]] = adapter_name + ":value" # FIXME i += 1 - elif channel.name == "Dimmer" or channel.name == "Intensity": + elif channel.name.lower() == "dimmer" or channel.name.lower() == "intensity": dimmer_name = _sanitize_name(f"dimmer_{i}_{name}") + double_channel_dimmer_required = any([ + ("dimmer" in fc.name.lower() or "intensity" in fc.name.lower()) and "fine" in fc.name.lower() + for fc in fixture.fixture_channels]) global_dimmer_filter = DimmerGlobalBrightnessMixinVFilter(scene=fp.parent_scene, filter_id=dimmer_name, pos=(int(x - 2 * _additional_filter_depth), int(compute_filter_height(channel_count, i)))) - global_dimmer_filter.filter_configurations["has_16bit_output"] = "true" - global_dimmer_filter.filter_configurations["has_8bit_output"] = "false" + if double_channel_dimmer_required: + global_dimmer_filter.filter_configurations["has_16bit_output"] = "true" + global_dimmer_filter.filter_configurations["has_8bit_output"] = "false" + else: + global_dimmer_filter.filter_configurations["has_16bit_output"] = "false" + global_dimmer_filter.filter_configurations["has_8bit_output"] = "true" global_dimmer_filter.deserialize() added_depth = max(added_depth, 2 * _additional_filter_depth) global_dimmer_found = True @@ -197,20 +204,28 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co dimmer_name = global_dimmer_filter.filter_id x += 10 - # TODO if we only have a single dimmer port we can output 8bit directly, reducing overhead; this - # requires having a dimmer feature in the fixture definition - adapter_name = _sanitize_name(f"dimmer2byte_{i}_{name}") - dimmer_to_byte_filter = Filter(scene=fp.parent_scene, - filter_id=adapter_name, - filter_type=8, - pos=(x - _additional_filter_depth, - compute_filter_height(channel_count, i))) - fp.parent_scene.append_filter(dimmer_to_byte_filter) - already_added_filters.append(dimmer_to_byte_filter) - adapter_name = dimmer_to_byte_filter.filter_id - dimmer_to_byte_filter.channel_links["value"] = dimmer_name + ":dimmer_out16b" - universe_filter.channel_links[_sanitize_name(channel.name)] = adapter_name + ":value_upper" - fp.filters.append(dimmer_to_byte_filter) + if double_channel_dimmer_required: + adapter_name = _sanitize_name(f"dimmer2byte_{i}_{name}") + dimmer_to_byte_filter = Filter(scene=fp.parent_scene, + filter_id=adapter_name, + filter_type=FilterTypeEnumeration.FILTER_ADAPTER_16BIT_TO_DUAL_8BIT, + pos=(x - _additional_filter_depth, + compute_filter_height(channel_count, i))) + fp.parent_scene.append_filter(dimmer_to_byte_filter) + already_added_filters.append(dimmer_to_byte_filter) + adapter_name = dimmer_to_byte_filter.filter_id + dimmer_to_byte_filter.channel_links["value"] = dimmer_name + ":dimmer_out16b" + fp.filters.append(dimmer_to_byte_filter) + + if double_channel_dimmer_required: + universe_filter.channel_links[_sanitize_name(channel.name)] = adapter_name + ":value_upper" + for fc in fixture.fixture_channels: + if (("dimmer" in fc.name.lower() or "intensity" in fc.name.lower()) + and "fine" in fc.name.lower()): + universe_filter.channel_links[_sanitize_name(fc.name)] = adapter_name + ":value_lower" + else: + universe_filter.channel_links[_sanitize_name(channel.name)] = dimmer_name + ":dimmer_out8b" + i += 1 except IndexError: continue From 756635b2345ee6b906e92cfb4c04eaf8ee02c2ec Mon Sep 17 00:00:00 2001 From: Doralitze Date: Tue, 16 Dec 2025 20:10:39 +0100 Subject: [PATCH 09/14] fix: redundant array copy --- src/view/show_mode/editor/show_browser/fixture_to_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/show_mode/editor/show_browser/fixture_to_filter.py b/src/view/show_mode/editor/show_browser/fixture_to_filter.py index 5a2601b8..eb5c042e 100644 --- a/src/view/show_mode/editor/show_browser/fixture_to_filter.py +++ b/src/view/show_mode/editor/show_browser/fixture_to_filter.py @@ -182,9 +182,9 @@ def compute_filter_height(channel_count_: int, filter_index: int, filter_chan_co i += 1 elif channel.name.lower() == "dimmer" or channel.name.lower() == "intensity": dimmer_name = _sanitize_name(f"dimmer_{i}_{name}") - double_channel_dimmer_required = any([ + double_channel_dimmer_required = any( ("dimmer" in fc.name.lower() or "intensity" in fc.name.lower()) and "fine" in fc.name.lower() - for fc in fixture.fixture_channels]) + for fc in fixture.fixture_channels) global_dimmer_filter = DimmerGlobalBrightnessMixinVFilter(scene=fp.parent_scene, filter_id=dimmer_name, pos=(int(x - 2 * _additional_filter_depth), From 113e9dff8c5b50ce7a745909b26b2a7444fe91e1 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Thu, 18 Dec 2025 18:18:45 +0100 Subject: [PATCH 10/14] add: towards unit test --- test/unittests/.gitignore | 1 + test/unittests/__init__.py | 0 .../test_dimmer_brightness_mixin_vfilter.py | 81 +++++++++++++++++++ test/unittests/utilities.py | 51 ++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 test/unittests/.gitignore create mode 100644 test/unittests/__init__.py create mode 100644 test/unittests/test_dimmer_brightness_mixin_vfilter.py create mode 100644 test/unittests/utilities.py diff --git a/test/unittests/.gitignore b/test/unittests/.gitignore new file mode 100644 index 00000000..f85c6b18 --- /dev/null +++ b/test/unittests/.gitignore @@ -0,0 +1 @@ +config.py \ No newline at end of file diff --git a/test/unittests/__init__.py b/test/unittests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unittests/test_dimmer_brightness_mixin_vfilter.py b/test/unittests/test_dimmer_brightness_mixin_vfilter.py new file mode 100644 index 00000000..197b7762 --- /dev/null +++ b/test/unittests/test_dimmer_brightness_mixin_vfilter.py @@ -0,0 +1,81 @@ +import unittest + +from model import BoardConfiguration, Scene, Filter +from model.filter import FilterTypeEnumeration +from model.virtual_filters.range_adapters import DimmerGlobalBrightnessMixinVFilter +from test.unittests.utilities import execute_board_configuratiuon + + +class DimmerBrightnessMixinTest(unittest.TestCase): + def _prepare_show_config(self) -> tuple[BoardConfiguration, list[tuple[str, float]]]: + show = BoardConfiguration() + scene = Scene(0, "Test Scene for dimmer brightness mixin", show) + show._add_scene(scene) + output_list = [] + row: int = 0 + + def create_input(method: str, mf: DimmerGlobalBrightnessMixinVFilter, is_mixin: bool) -> str: + mf.filter_configurations["input_method_mixin" if is_mixin else "input_method"] = method.replace("-", "") + if "-" not in method: + input_filter = Filter( + scene, + f"input_filter_{row}_{method}_{"mixin" if is_mixin else "input"}", + FilterTypeEnumeration.FILTER_CONSTANT_8BIT if method == "8bit" else FilterTypeEnumeration.FILTER_CONSTANT_16_BIT, + pos=(-10, row * 15 + (5 if is_mixin else -5)) + ) + input_filter.initial_parameters["value"] = str(255 / 2) if method == "8bit" else str(65565 / 2) + scene.append_filter(input_filter) + mf.channel_links["mixin" if is_mixin else "input"] = f"{input_filter.filter_id}:value" + + def create_output(is_8b: bool): + output_filter = Filter( + scene, f"output_8b_{row}", FilterTypeEnumeration.FILTER_REMOTE_DEBUG_8BIT if is_8b else FilterTypeEnumeration.FILTER_REMOTE_DEBUG_16BIT, + pos=(5, row * 15 + (-5 if is_8b else 5)) + ) + scene.append_filter(output_filter) + output_filter.channel_links["value"] = mixin_filter.filter_id + ":dimmer_out8b" + output_list.append( + (output_filter.filter_id, (255 if is_8b else 65565) * calculate_dimmer_val(input_method, mixin_method, has_offset))) + + def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: + in_stream = 1.0 if "-" in in_m else 0.5 + mixin_stream = 1.0 + offset_stream = 0.25 if has_offset else 0.0 + return max(0.0, min(1.0, in_stream * mixin_stream + offset_stream)) + + for input_method in ["-8bit", "-16bit", "8bit", "16bit"]: + for mixin_method in ["-8bit", "-16bit", "8bit", "16bit"]: + for output_8b_enabled in [True, False]: + for output_16b_enabled in [True, False]: + for has_offset in [True, False]: + mixin_filter = DimmerGlobalBrightnessMixinVFilter(scene, f"mixin_filter_r{row}", (0, row * 15)) + create_input(input_method, mixin_filter, False) + create_input(mixin_method, mixin_filter, True) + + if has_offset: + offset_filter = Filter( + scene, f"offset_filter_{row}", FilterTypeEnumeration.FILTER_CONSTANT_FLOAT, + pos=(-5, row * 15) + ) + offset_filter.initial_parameters["value"] = "0.25" + scene.append_filter(offset_filter) + mixin_filter.channel_links["offset"] = offset_filter.filter_id + ":value" + + if output_8b_enabled: + create_output(True) + if output_16b_enabled: + create_output(False) + row += 1 + return show, output_list + + def test_inst(self): + show, expected_output_list = self._prepare_show_config() + recorded_output_list = [] + expected_output_dict = {} + for key, expected_val in expected_output_list: + expected_output_dict[key] = expected_val + self.assertTrue(execute_board_configuratiuon(show, recorded_gui_updates=recorded_output_list)) + for scene_id, filter_id, value_str in recorded_output_list: + self.assertEqual(scene_id, 0, "Expected scene ID to be 0.") + self.assertTrue(expected_output_dict[filter_id] - 5 < int(value_str) < expected_output_dict[filter_id] + 5, + f"Expected configuration of {filter_id} to be {expected_output_dict[filter_id]} but got {value_str} instead.") diff --git a/test/unittests/utilities.py b/test/unittests/utilities.py new file mode 100644 index 00000000..388b7ace --- /dev/null +++ b/test/unittests/utilities.py @@ -0,0 +1,51 @@ +"""Unit test utils. + +This module requires a config.py to be in the same package. This config file needs to contain a variable called +FISH_EXEC_PATH pointing to a local fish binary. +""" +import subprocess + +from model import BoardConfiguration + +from .config import FISH_EXEC_PATH + +def _stall_until_stdout_reached_target(process: subprocess.Popen, target: str): + while True: + output = process.stdout.read(1024) + if not output and process.poll() is not None: + return False + if target in output.decode(): + return True + + +def execute_board_configuratiuon(bc: BoardConfiguration, cycles: int = 25, recorded_gui_updates: list[tuple[int, str, str]] | None = None, main_brightness: int = 65565) -> bool: + """Execute a board configuration. + + This method starts a fish instance, connects to it, uploads a board configuration and enables filter execution. + If after the specified amount of iterations, no error occurred, the method stops, returning true. Otherwise false. + + Args: + bc: The board configuration to apply + cycles: The amount of cycles to wait + recorded_gui_updates: If A list is provided, any GUI updates received during execution are stored in there. + main_brightness: The Main brightness value used for the test. + + Returns: + True if no error occurred during execution. Otherwise, false. + + """ + process = subprocess.Popen( + [FISH_EXEC_PATH], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) + _stall_until_stdout_reached_target(process, "[debug] Entering ev defloop") + # TODO create network manager instance + # TODO call send to fish method + # TODO wait for response and abort if error received + # TODO set main_brightness + # TODO wait for cycles * 40 ms and record gui messages + # TODO disconnect client and close QT Application + # TODO stop fish by sending enter to stdin + # TODO wait for fish to stop and return result From 61e0e8251368dc5b02ff9cf95ba6e5e7a7c23ea1 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Thu, 18 Dec 2025 22:57:31 +0100 Subject: [PATCH 11/14] add: execute_board_configuration method --- src/main.py | 4 +- .../test_dimmer_brightness_mixin_vfilter.py | 21 +++-- test/unittests/utilities.py | 90 ++++++++++++++++--- 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/main.py b/src/main.py index 2dc7556d..11bf6fbe 100644 --- a/src/main.py +++ b/src/main.py @@ -2,12 +2,13 @@ if __name__ == "__main__": import os + import sys from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication, QSplashScreen QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL) - app = QApplication([]) + app = QApplication(sys.argv) from PySide6.QtGui import QPixmap from utility import resource_path @@ -44,7 +45,6 @@ import logging.config import logging.handlers import pathlib - import sys from PySide6.QtCore import QEventLoop diff --git a/test/unittests/test_dimmer_brightness_mixin_vfilter.py b/test/unittests/test_dimmer_brightness_mixin_vfilter.py index 197b7762..13931ca6 100644 --- a/test/unittests/test_dimmer_brightness_mixin_vfilter.py +++ b/test/unittests/test_dimmer_brightness_mixin_vfilter.py @@ -1,9 +1,14 @@ +import logging import unittest +from logging import getLogger, basicConfig from model import BoardConfiguration, Scene, Filter from model.filter import FilterTypeEnumeration from model.virtual_filters.range_adapters import DimmerGlobalBrightnessMixinVFilter -from test.unittests.utilities import execute_board_configuratiuon +from test.unittests.utilities import execute_board_configuration + + +logger = getLogger(__name__) class DimmerBrightnessMixinTest(unittest.TestCase): @@ -29,13 +34,15 @@ def create_input(method: str, mf: DimmerGlobalBrightnessMixinVFilter, is_mixin: def create_output(is_8b: bool): output_filter = Filter( - scene, f"output_8b_{row}", FilterTypeEnumeration.FILTER_REMOTE_DEBUG_8BIT if is_8b else FilterTypeEnumeration.FILTER_REMOTE_DEBUG_16BIT, + scene, f"output_{"8b" if is_8b else "16b"}_{row}", FilterTypeEnumeration.FILTER_REMOTE_DEBUG_8BIT if is_8b else FilterTypeEnumeration.FILTER_REMOTE_DEBUG_16BIT, pos=(5, row * 15 + (-5 if is_8b else 5)) ) scene.append_filter(output_filter) - output_filter.channel_links["value"] = mixin_filter.filter_id + ":dimmer_out8b" + output_filter.channel_links["value"] = mixin_filter.filter_id + (":dimmer_out8b" if is_8b else ":dimmer_out16b") + expected_output = (255 if is_8b else 65565) * calculate_dimmer_val(input_method, mixin_method, has_offset) + logger.info("Registering output %s => %s.", output_filter.filter_id, expected_output) output_list.append( - (output_filter.filter_id, (255 if is_8b else 65565) * calculate_dimmer_val(input_method, mixin_method, has_offset))) + (output_filter.filter_id, expected_output)) def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: in_stream = 1.0 if "-" in in_m else 0.5 @@ -48,6 +55,7 @@ def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: for output_8b_enabled in [True, False]: for output_16b_enabled in [True, False]: for has_offset in [True, False]: + logger.info("Creating Config Row: %s, Input: %s, Mixin: %s, Out bit: %s, Out 16bit: %s, has Offset: %s", row, input_method, mixin_method, output_8b_enabled, output_16b_enabled, has_offset) mixin_filter = DimmerGlobalBrightnessMixinVFilter(scene, f"mixin_filter_r{row}", (0, row * 15)) create_input(input_method, mixin_filter, False) create_input(mixin_method, mixin_filter, True) @@ -69,13 +77,14 @@ def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: return show, output_list def test_inst(self): + basicConfig(level=logging.DEBUG) show, expected_output_list = self._prepare_show_config() recorded_output_list = [] expected_output_dict = {} for key, expected_val in expected_output_list: expected_output_dict[key] = expected_val - self.assertTrue(execute_board_configuratiuon(show, recorded_gui_updates=recorded_output_list)) - for scene_id, filter_id, value_str in recorded_output_list: + self.assertTrue(execute_board_configuration(show, recorded_gui_updates=recorded_output_list)) + for scene_id, filter_id, key, value_str in recorded_output_list: self.assertEqual(scene_id, 0, "Expected scene ID to be 0.") self.assertTrue(expected_output_dict[filter_id] - 5 < int(value_str) < expected_output_dict[filter_id] + 5, f"Expected configuration of {filter_id} to be {expected_output_dict[filter_id]} but got {value_str} instead.") diff --git a/test/unittests/utilities.py b/test/unittests/utilities.py index 388b7ace..539fa372 100644 --- a/test/unittests/utilities.py +++ b/test/unittests/utilities.py @@ -3,26 +3,40 @@ This module requires a config.py to be in the same package. This config file needs to contain a variable called FISH_EXEC_PATH pointing to a local fish binary. """ +import os.path import subprocess +from logging import getLogger +from time import sleep -from model import BoardConfiguration +from PySide6.QtWidgets import QApplication + +import proto.FilterMode_pb2 +from controller.file.serializing.general_serialization import create_xml +from controller.network import NetworkManager +from controller.utils.process_notifications import get_process_notifier +from model import BoardConfiguration, Broadcaster from .config import FISH_EXEC_PATH +logger = getLogger(__name__) + def _stall_until_stdout_reached_target(process: subprocess.Popen, target: str): while True: - output = process.stdout.read(1024) + output = process.stdout.readline() if not output and process.poll() is not None: return False - if target in output.decode(): + if target in output: return True -def execute_board_configuratiuon(bc: BoardConfiguration, cycles: int = 25, recorded_gui_updates: list[tuple[int, str, str]] | None = None, main_brightness: int = 65565) -> bool: +_last_error_message = "" +_GOOD_MESSAGES = ["No Error occured", "Showfile Applied"] + +def execute_board_configuration(bc: BoardConfiguration, cycles: int = 25, recorded_gui_updates: list[tuple[int, str, str, str]] | None = None, main_brightness: int = 65565) -> bool: """Execute a board configuration. This method starts a fish instance, connects to it, uploads a board configuration and enables filter execution. - If after the specified amount of iterations, no error occurred, the method stops, returning true. Otherwise false. + If after the specified amount of iterations, no error occurred, the method stops, returning true. Otherwise, false. Args: bc: The board configuration to apply @@ -34,18 +48,68 @@ def execute_board_configuratiuon(bc: BoardConfiguration, cycles: int = 25, recor True if no error occurred during execution. Otherwise, false. """ + global _last_error_message + _last_error_message = _GOOD_MESSAGES[0] + logger.debug("Starting fish...") + if os.path.exists("/tmp/fish.sock"): + logger.warning("Removing fish socket.") + os.remove("/tmp/fish.sock") process = subprocess.Popen( [FISH_EXEC_PATH], stdout=subprocess.PIPE, + stdin=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True ) _stall_until_stdout_reached_target(process, "[debug] Entering ev defloop") - # TODO create network manager instance - # TODO call send to fish method - # TODO wait for response and abort if error received - # TODO set main_brightness - # TODO wait for cycles * 40 ms and record gui messages - # TODO disconnect client and close QT Application - # TODO stop fish by sending enter to stdin - # TODO wait for fish to stop and return result + logger.debug("Starting QApplication...") + application = QApplication([]) + logger.debug("Starting network manager.") + nm = NetworkManager() + def fish_error_received(msg: str): + global _last_error_message + if msg != _last_error_message: + if msg not in _GOOD_MESSAGES: + logger.error("Received error from fish: '%s'", msg) + _last_error_message = msg + nm.status_updated.connect(fish_error_received) + broadcaster = Broadcaster() + application.processEvents() + logger.debug("Connecting to fish...") + nm.start() + application.processEvents() + error_occurred: bool = False + + def receive_update_from_fish(msg: proto.FilterMode_pb2.update_parameter): + if recorded_gui_updates is not None: + recorded_gui_updates.append((msg.scene_id, msg.filter_id, msg.parameter_key, msg.parameter_value)) + + broadcaster.update_filter_parameter.connect(receive_update_from_fish) + for i in range(10): + application.processEvents() + sleep(0.01) + pn = get_process_notifier("test upload notifier", 300) + logger.info("Creating Show XML...") + xml = create_xml(bc, pn, assemble_for_fish_loading=True) + logger.info("Sending it to fish...") + nm.transmit_show_file(xml, True) + while _last_error_message == _GOOD_MESSAGES[0] and nm.connection_state(): + application.processEvents() + sleep(0.01) + if _last_error_message != _GOOD_MESSAGES[1]: + error_occurred = True + nm.set_main_brightness_fader_position(int((main_brightness / 65565) * 255)) + logger.info("Evaluating execution...") + if not error_occurred: + for i in range(cycles * 4): + application.processEvents() + sleep(0.01) + logger.info("Ending execution...") + nm.disconnect() + sleep(1) + del broadcaster + del nm + del application + process.stdin.write("k\n") + process.communicate() + return process.returncode == 0 and not error_occurred From b1e80e5020a0d875c195bad2631737332290abea Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Fri, 19 Dec 2025 16:23:20 +0100 Subject: [PATCH 12/14] fix: documentation in new unit tests --- test/unittests/__init__.py | 1 + test/unittests/test_dimmer_brightness_mixin_vfilter.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/test/unittests/__init__.py b/test/unittests/__init__.py index e69de29b..dc2612aa 100644 --- a/test/unittests/__init__.py +++ b/test/unittests/__init__.py @@ -0,0 +1 @@ +"""Unit test collection.""" \ No newline at end of file diff --git a/test/unittests/test_dimmer_brightness_mixin_vfilter.py b/test/unittests/test_dimmer_brightness_mixin_vfilter.py index 13931ca6..d5f64f86 100644 --- a/test/unittests/test_dimmer_brightness_mixin_vfilter.py +++ b/test/unittests/test_dimmer_brightness_mixin_vfilter.py @@ -1,3 +1,4 @@ +"""Unit test for dimmer brightness mixin.""" import logging import unittest from logging import getLogger, basicConfig @@ -12,6 +13,8 @@ class DimmerBrightnessMixinTest(unittest.TestCase): + """Unit test for dimmer brightness mixin.""" + def _prepare_show_config(self) -> tuple[BoardConfiguration, list[tuple[str, float]]]: show = BoardConfiguration() scene = Scene(0, "Test Scene for dimmer brightness mixin", show) @@ -77,6 +80,7 @@ def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: return show, output_list def test_inst(self): + """Test instanciation and results of v filter.""" basicConfig(level=logging.DEBUG) show, expected_output_list = self._prepare_show_config() recorded_output_list = [] From eebcbb79226b23328df6a7866fd88a20944aac64 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sat, 20 Dec 2025 15:41:25 +0100 Subject: [PATCH 13/14] fix: bug causing show file loading to not work --- src/model/virtual_filters/range_adapters.py | 7 +++++-- test/unittests/test_dimmer_brightness_mixin_vfilter.py | 4 +++- test/unittests/utilities.py | 7 +++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/model/virtual_filters/range_adapters.py b/src/model/virtual_filters/range_adapters.py index 1ae788c6..511f500b 100644 --- a/src/model/virtual_filters/range_adapters.py +++ b/src/model/virtual_filters/range_adapters.py @@ -145,7 +145,7 @@ def resolve_output_port_id(self, virtual_port_id: str) -> str | None: out_8b = self.filter_configurations.get("has_8bit_output") == "true" if virtual_port_id == "dimmer_out8b": if out_8b and out_16b: - return f"{self.filter_id}_16b_downsampler:upper" + return f"{self.filter_id}_16b_downsampler:value_upper" if out_8b: return f"{self._filter_id}_8b_range:value" raise ValueError("Requested 8bit output port but 8bit output is disabled.") @@ -165,7 +165,7 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: needs_const_mixin = self.channel_links.get("mixin") is None needs_offset = self.channel_links.get("offset") is None - if needs_const_mixin and (not needs_global_brightness_input and not needs_offset): + if needs_const_mixin and (not needs_global_brightness_input or not needs_offset): const_mixin_filter = Filter( self.scene, f"{self.filter_id}_const_mixin", @@ -176,6 +176,9 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None: filter_list.append(const_mixin_filter) mixin_port_name = f"{self.filter_id}_const_mixin:value" required_mixin_input_method = 0 + elif needs_const_mixin: + required_mixin_input_method = 0 + mixin_port_name = None else: mixin_port_name = self.channel_links.get("mixin") diff --git a/test/unittests/test_dimmer_brightness_mixin_vfilter.py b/test/unittests/test_dimmer_brightness_mixin_vfilter.py index d5f64f86..42480a73 100644 --- a/test/unittests/test_dimmer_brightness_mixin_vfilter.py +++ b/test/unittests/test_dimmer_brightness_mixin_vfilter.py @@ -58,7 +58,7 @@ def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: for output_8b_enabled in [True, False]: for output_16b_enabled in [True, False]: for has_offset in [True, False]: - logger.info("Creating Config Row: %s, Input: %s, Mixin: %s, Out bit: %s, Out 16bit: %s, has Offset: %s", row, input_method, mixin_method, output_8b_enabled, output_16b_enabled, has_offset) + logger.info("Creating Config Row: %s, Input: %s, Mixin: %s, Out 8bit: %s, Out 16bit: %s, has Offset: %s", row, input_method, mixin_method, output_8b_enabled, output_16b_enabled, has_offset) mixin_filter = DimmerGlobalBrightnessMixinVFilter(scene, f"mixin_filter_r{row}", (0, row * 15)) create_input(input_method, mixin_filter, False) create_input(mixin_method, mixin_filter, True) @@ -72,6 +72,8 @@ def calculate_dimmer_val(in_m: str, mixin_m: str, has_offset: bool) -> float: scene.append_filter(offset_filter) mixin_filter.channel_links["offset"] = offset_filter.filter_id + ":value" + scene.append_filter(mixin_filter) + if output_8b_enabled: create_output(True) if output_16b_enabled: diff --git a/test/unittests/utilities.py b/test/unittests/utilities.py index 539fa372..0a6d6a5d 100644 --- a/test/unittests/utilities.py +++ b/test/unittests/utilities.py @@ -30,7 +30,7 @@ def _stall_until_stdout_reached_target(process: subprocess.Popen, target: str): _last_error_message = "" -_GOOD_MESSAGES = ["No Error occured", "Showfile Applied"] +_GOOD_MESSAGES = ["No Error occured", "Showfile Applied."] def execute_board_configuration(bc: BoardConfiguration, cycles: int = 25, recorded_gui_updates: list[tuple[int, str, str, str]] | None = None, main_brightness: int = 65565) -> bool: """Execute a board configuration. @@ -97,6 +97,7 @@ def receive_update_from_fish(msg: proto.FilterMode_pb2.update_parameter): application.processEvents() sleep(0.01) if _last_error_message != _GOOD_MESSAGES[1]: + logger.error("Final error state from fish was: %s. Failing execution.", _last_error_message) error_occurred = True nm.set_main_brightness_fader_position(int((main_brightness / 65565) * 255)) logger.info("Evaluating execution...") @@ -112,4 +113,6 @@ def receive_update_from_fish(msg: proto.FilterMode_pb2.update_parameter): del application process.stdin.write("k\n") process.communicate() - return process.returncode == 0 and not error_occurred + if process.returncode != 0: + logger.error("Fish terminated with code %s", process.returncode) + return not error_occurred From 6c6213147aa733b8cd61ee1f4f268c92c99b4439 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Wed, 7 Jan 2026 15:47:34 +0100 Subject: [PATCH 14/14] add: improved show file upload error handling to editor --- src/model/ofl/fixture.py | 5 ++++- src/model/virtual_filters/range_adapters.py | 4 ++-- src/view/show_mode/editor/show_browser/show_browser.py | 9 ++++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/model/ofl/fixture.py b/src/model/ofl/fixture.py index 129b197e..035d5141 100644 --- a/src/model/ofl/fixture.py +++ b/src/model/ofl/fixture.py @@ -61,7 +61,10 @@ def load_fixture(file: str) -> OflFixture | None: logger.error("Fixture definition %s not found.", file) return None with open(file, "r", encoding="UTF-8") as f: - ob: dict = json.load(f) + try: + ob: dict = json.load(f) + except json.decoder.JSONDecodeError as e: + logger.error("Fixture definition (%s) JSON error: %s", file, e) ob.update({"fileName": file.split("/fixtures/")[1]}) return OflFixture.model_validate(ob) diff --git a/src/model/virtual_filters/range_adapters.py b/src/model/virtual_filters/range_adapters.py index 511f500b..1575c340 100644 --- a/src/model/virtual_filters/range_adapters.py +++ b/src/model/virtual_filters/range_adapters.py @@ -148,11 +148,11 @@ def resolve_output_port_id(self, virtual_port_id: str) -> str | None: return f"{self.filter_id}_16b_downsampler:value_upper" if out_8b: return f"{self._filter_id}_8b_range:value" - raise ValueError("Requested 8bit output port but 8bit output is disabled.") + raise ValueError(f"Requested 8bit output port but 8bit output is disabled. Filter ID: {self.filter_id}") if virtual_port_id == "dimmer_out16b": if out_16b: return f"{self._filter_id}_16b_range:value" - raise ValueError("Requested 8bit output port but 16bit output is disabled.") + raise ValueError(f"Requested 16bit output port but 16bit output is disabled. Filter ID: {self.filter_id}") raise ValueError("Unknown output port") @override diff --git a/src/view/show_mode/editor/show_browser/show_browser.py b/src/view/show_mode/editor/show_browser/show_browser.py index 3de3ddf0..a7a50891 100644 --- a/src/view/show_mode/editor/show_browser/show_browser.py +++ b/src/view/show_mode/editor/show_browser/show_browser.py @@ -343,7 +343,14 @@ def _universe_item_double_clicked(self, item: QTreeWidgetItem) -> None: current_widget.refresh() def _upload_showfile(self) -> None: - transmit_to_fish(self._show, False) + try: + transmit_to_fish(self._show, False) + except ValueError as e: + self._input_dialog = QMessageBox(QMessageBox.Icon.Critical, "Uploading Showfile Failed", + "An error occurred while uploading the show file to fish:", + parent=self.widget, detailedText=str(e)) + self._input_dialog.setModal(True) + self._input_dialog.show() def _duplicate_scene(self, selected_items: list[QTreeWidgetItem]) -> None: i: int = 1