def animate_horizontal(self): tally_types = [t for t in TallyType if t != TallyType.all_tally] while tally_types[0] != self.cur_group: t = tally_types.pop(0) tally_types.append(t) for i, t in enumerate(tally_types): if t == TallyType.no_tally: continue tg = self.tally_groups[t] tg.reset_all() try: color = TallyColor(i+1) except ValueError: color = TallyColor.OFF tg.tally_colors[self.cur_index] = color try: if self.cur_group.value == 0: v = 1 else: v = self.cur_group.value * 2 if v > TallyType.all_tally.value: raise ValueError() t = TallyType(v) self.cur_group = t except ValueError: self.cur_index += 1 self.cur_group = TallyType.no_tally if self.cur_index >= self.num_tallies: self.set_animate_mode(AnimateMode.vertical)
def unmatched_sconfs(unmatched_ttypes): return [[ SingleTallyConfig(screen_index=0, tally_index=0, tally_type=TallyType.from_str(ttype)) for ttype in ttypes ] for ttypes in unmatched_ttypes]
def __getitem__(self, key: StrOrTallyType) -> TallyColor: if not isinstance(key, TallyType): key = TallyType.from_str(key) if key.is_iterable: color = TallyColor.OFF for tt in key: color |= getattr(self, tt.name) return color return getattr(self, key.name)
def test_single_tally_matching(): tally_type = TallyType.rh_tally for i in range(10): all_screens = SingleTallyConfig( screen_index=None, tally_index=i, tally_type=tally_type, ) assert all_screens.matches_screen(0xffff) for j in range(10): assert all_screens.matches_screen(j) tconf0 = SingleTallyConfig( screen_index=j, tally_index=i, tally_type=tally_type, ) tconf1 = SingleTallyConfig( screen_index=j, tally_index=i + 1, tally_type=tally_type, ) tconf2 = SingleTallyConfig( screen_index=j + 1, tally_index=i, tally_type=tally_type, ) tconfs = [tconf0, tconf1, tconf2] for tconf in tconfs: assert tconf.matches(tconf.id, tconf.tally_type) assert tconf.matches( tconf.id, tconf.tally_type, return_matched=True) is tconf assert tconf.matches(tconf, tally_type=TallyType.from_str('rh|lh')) assert not tconf.matches(tconf, tally_type=TallyType.txt_tally) assert tconf.matches_screen(0xffff) assert tconf.matches_screen(all_screens) assert all_screens.matches_screen(tconf) if tconf.tally_index == i: assert tconf.matches(all_screens) assert tconf.matches(all_screens.id, all_screens.tally_type) else: assert not tconf.matches(all_screens) assert not tconf.matches(all_screens.id, all_screens.tally_type) assert tconf0.matches_screen(tconf1) assert tconf0.matches_screen(j) assert not tconf0.matches_screen(tconf2) assert not tconf0.matches_screen(j + 1) assert not tconf0.matches(tconf1) assert not tconf0.matches(tconf1.id, tconf1.tally_type) assert not tconf0.matches(tconf2) assert not tconf0.matches(tconf2.id, tconf2.tally_type)
def __setitem__(self, key: StrOrTallyType, value: StrOrTallyColor): if not isinstance(key, TallyType): key = TallyType.from_str(key) if not isinstance(value, TallyColor): value = TallyColor.from_str(value) if key.is_iterable: for tt in key: setattr(self, tt.name, value) else: setattr(self, key.name, value)
def __init__(self, clients=None, num_tallies=8, update_interval=.5, screen=1, all_off_on_close=False): self.num_tallies = num_tallies self.update_interval = update_interval super().__init__(clients, all_off_on_close) self.screen = self.get_or_create_screen(screen) for i in range(self.num_tallies): self.screen.add_tally(i, text=string.ascii_uppercase[i]) self.tally_groups = {} for tally_type in TallyType.all(): tg = TallyTypeGroup(tally_type, self.num_tallies) self.tally_groups[tally_type] = tg
async def on_receiver_tally_change(self, tally: Tally, props_changed: Set[str], **kwargs): changed = set() for prop in props_changed: if prop not in TallyType.__members__: continue ttype = TallyType.from_str(prop) pixel = self.tally_type_map.get(tally.id + (ttype, )) color = self.get_merged_tally(tally, ttype) if color == self.get(pixel): continue self[pixel] = color changed.add(pixel) await self.queue_update(*changed)
def get_init_options(cls) -> Tuple[Option]: tt_choices = tuple((tt.name for tt in TallyType)) return ( Option(name='tally_index', type=int, required=True, title='Index'), Option( name='tally_type', type=str, required=True, choices=tt_choices, serialize_cb=lambda x: x.to_str(), validate_cb=lambda x: TallyType.from_str(x), title='TallyType', ), TallyColorOption, Option(name='screen_index', type=int, required=False, title='Screen'), Option(name='name', type=str, required=False, default='', title='Name'), )
async def wait_for_rx(tally_type): tally_types = set() if not isinstance(tally_type, TallyType): tally_type = TallyType.from_str(tally_type) if tally_type.is_iterable: for tt in tally_type: tally_types.add(tt.name) else: tally_types.add(tally_type.name) props = set() for _ in range(len(tally_types)): evt_args, evt_kwargs = await tally_listener.get() props |= evt_args[1] if props == tally_types: break assert props == tally_types
def build_multi_config(self) -> MultiTallyConfig: self.tally_type_map.clear() tconfs = [] scr = self.config.screen_index start_index = self.config.tally_index for i in range(5): tally_index = start_index + i for j, ttype in enumerate(TallyType.all()): pixel = (j, i) tconf = SingleTallyConfig(screen_index=scr, tally_index=tally_index, tally_type=ttype) tconfs.append(tconf) key = tconf.tally_key + (ttype, ) self.tally_type_map[key] = pixel self.multi_config = MultiTallyConfig(tallies=tconfs)
def decode(self, d): if '__class__' in d: cls = self.str_to_cls(d['__class__']) if cls is not None: if cls is DeviceMapping: for key in ['program', 'preview']: if not isinstance(d[key], TallyMap): d[key] = self.decode(d[key]) elif cls is TallyMap: if not isinstance(d['tally_type'], TallyType): if isinstance(d['tally_type'], int): d['tally_type'] = TallyType(d['tally_type']) else: d['tally_type'] = self.decode(d['tally_type']) elif cls is TallyType: return getattr(TallyType, d['name']) del d['__class__'] return cls(**d) return d
async def test_all_off_on_close(faker, udp_port): loop = asyncio.get_event_loop() sender = UmdSender( clients=[('127.0.0.1', udp_port)], all_off_on_close=True, ) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) add_listener = EventListener() receiver.bind_async(loop, on_tally_added=add_listener.callback) tally_listener = EventListener() receiver.bind_async(loop, on_tally_updated=tally_listener.callback) async with receiver: async with sender: for screen_index in range(10): for i in range(10): t_id = (screen_index, i) sender.set_tally_text(t_id, f'Tally-{i}') tx_tally = sender.tallies[t_id] evt_args, evt_kwargs = await add_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally for ttype in TallyType.all(): setattr(tx_tally, ttype.name, TallyColor.RED) evt_args, evt_kwargs = await tally_listener.get() assert getattr(rx_tally, ttype.name) == TallyColor.RED # Sender is closed and should have broadcast "all-off" _ = await asyncio.wait_for(tally_listener.get(), timeout=1) while not tally_listener.empty(): _ = await tally_listener.get() for rx_tally in receiver.tallies.values(): assert rx_tally.rh_tally == TallyColor.OFF assert rx_tally.txt_tally == TallyColor.OFF assert rx_tally.lh_tally == TallyColor.OFF
def test_memoization(): mconf0 = MultiTallyConfig(screen_index=None, allow_all=True) mconf1 = MultiTallyConfig() for i in range(8): for j in range(16): for tally_type in TallyType.all(): conf = SingleTallyConfig( screen_index=i, tally_index=j, tally_type=tally_type, ) mconf1.tallies.append(conf) for i in range(8): screen = None for j in range(16): t_id = (i, j) tally_type = TallyType.rh_tally sconf0 = SingleTallyConfig( screen_index=i, tally_index=j, tally_type=tally_type, ) match = mconf0.matches(sconf0, return_matched=True) assert match is sconf0 match = mconf0.matches(sconf0, return_matched=True) assert match is sconf0 match = mconf0.matches(sconf0.id, tally_type, return_matched=True) assert match is sconf0 tally_type = TallyType.txt_tally sconf1 = mconf0.matches(t_id, tally_type, return_matched=True) assert sconf1.id == t_id assert sconf1.tally_type == tally_type assert sconf1 is not sconf0 match = mconf0.matches(sconf1, return_matched=True) assert match is sconf1 match = mconf0.matches(sconf1.id, tally_type, return_matched=True) assert match is sconf1 match = mconf0.matches(sconf1, tally_type, return_matched=True) assert match is sconf1 tally_type = TallyType.lh_tally tmp_conf = SingleTallyConfig( screen_index=i, tally_index=j, tally_type=tally_type, ) screen, tally = tmp_conf.create_tally(screen) assert tally.id == t_id sconf2 = mconf0.matches(tally, tally_type, return_matched=True) assert sconf2.id == t_id assert sconf2.tally_type == tally_type for sconf in [sconf0, sconf1, sconf2]: match = mconf1.matches(sconf, return_matched=True) assert match == sconf assert match is not sconf assert match in mconf1.tallies assert mconf1.search_memoized(sconf) is match assert mconf1.search_memoized(sconf.id, sconf.tally_type) is match match = mconf1.search_memoized(tally, sconf2.tally_type) assert match == sconf2 assert mconf0.memoized_tally_confs == mconf1.memoized_tally_confs
async def test_with_uhs_data(uhs500_msg_bytes, uhs500_msg_parsed, udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) uhs_screen = uhs500_msg_parsed.screen async with receiver: # Send message bytes to receiver transport.sendto(uhs500_msg_bytes, ('127.0.0.1', udp_port)) # Wait for all ``on_tally_added`` events _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() screen = receiver.screens[uhs_screen] # Check all receiver tallies against the expected ones assert len(receiver.tallies) == len(uhs500_msg_parsed.displays) for disp in uhs500_msg_parsed.displays: assert disp.index in screen tally = screen.tallies[disp.index] assert tally.id == (uhs_screen, disp.index) assert receiver.tallies[tally.id] is tally assert disp == tally # Change each display and send the updated message to receiver # Then wait for ``on_tally_updated`` events receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) for disp in uhs500_msg_parsed.displays: tally = screen.tallies[disp.index] for tally_type in TallyType.all(): attr = tally_type.name cur_value = getattr(disp, attr) if cur_value == TallyColor.RED: new_value = TallyColor.GREEN else: new_value = TallyColor.RED setattr(disp, attr, new_value) disp.text = f'{disp.text}-foo' disp.brightness = 1 transport.sendto(uhs500_msg_parsed.build_message(), ('127.0.0.1', udp_port)) evt_args, evt_kwargs = await evt_listener.get() evt_tally = evt_args[0] assert evt_tally is tally assert disp == tally
async def test_with_uhs_data(udp_port): loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', udp_port)]) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) screen_index = 1 async with receiver: async with sender: # Create initial tallies using text method for i in range(100): t_id = (screen_index, i) sender.set_tally_text(t_id, f'Tally-{i}') tx_tally = sender.tallies[t_id] screen = sender.screens[screen_index] assert screen[i] is tx_tally evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Create one more tally using ``set_tally_color`` t_id = (screen_index, 200) sender.set_tally_color(t_id, TallyType.lh_tally, TallyColor.GREEN) tx_tally = sender.tallies[t_id] assert screen[200] is tx_tally evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Allow the sender to do a full refresh. Nothing should have changed await asyncio.sleep(sender.tx_interval) assert evt_listener.empty() # Connect to ``on_tally_updated`` events receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) # Change each tally/tally_type color to red and check the received values for tx_tally in sender.tallies.values(): for tally_type in TallyType.all(): sender.set_tally_color(tx_tally.id, tally_type, TallyColor.RED) evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally is receiver.tallies[tx_tally.id] assert rx_tally == tx_tally # Change the text of the extra tally from above and check t_id = (screen_index, 200) sender.set_tally_text(t_id, 'Tally-200') tx_tally = sender.tallies[t_id] evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Let the sender to another full refresh await asyncio.sleep(sender.tx_interval) assert evt_listener.empty() # Change all tally/tally_type colors, but don't wait for results yet for tx_tally in sender.tallies.values(): for tally_type in TallyType.all(): sender.set_tally_color(tx_tally.id, tally_type, TallyColor.AMBER) sender.set_tally_text(tx_tally.id, f'foo-{tx_tally.index}') # Wait for updates from last loop to get to the receiver # and check the results _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() for tx_tally in sender.tallies.values(): rx_tally = receiver.tallies[tx_tally.id] assert rx_tally == tx_tally
def iter_tally_types_and_colors(): yield from itertools.product(TallyType.all(), iter_tally_colors())
"""The client data as a tuple of (:attr:`hostaddr`, :attr:`hostport`) """ return (self.hostaddr, self.hostport) ClientsOption = ListOption( name='clients', type=ClientData, required=False, sub_options=ClientData.get_init_options(), title='Clients', ) ClientOrData = Union[Client, ClientData] """:data:`~tslumd.sender.Client` or :class:`ClientData` """ INDICATOR_PROPS = {tt.name: tt for tt in TallyType.all()} class UmdOutput(BaseOutput, namespace='umd.UmdOutput', final=True): """Networked tally output using the UMDv5 protocol Arguments: config (MultiTallyConfig): The initial value for :attr:`~tallypi.baseio.BaseIO.config` clients (Iterable[ClientOrData], optional): The initial :attr:`clients` to set all_off_on_close: (bool, optional): Value to set for :attr:`all_off_on_close` Properties: clients (set): A :class:`~.utils.SetProperty` containing the remote host addresses as address/port tuples.
def from_dict(cls, d: Dict) -> 'SingleTallyConfig': kw = d.copy() if not isinstance(kw['tally_type'], TallyType): kw['tally_type'] = TallyType.from_str(kw['tally_type']) return super().from_dict(kw)