Skip to content

Commit 9235203

Browse files
committed
Initial commit
0 parents  commit 9235203

File tree

12 files changed

+1112
-0
lines changed

12 files changed

+1112
-0
lines changed

.gitignore

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*,cover
46+
.hypothesis/
47+
48+
# Translations
49+
*.mo
50+
*.pot
51+
52+
# Django stuff:
53+
*.log
54+
local_settings.py
55+
56+
# Flask stuff:
57+
instance/
58+
.webassets-cache
59+
60+
# Scrapy stuff:
61+
.scrapy
62+
63+
# Sphinx documentation
64+
docs/_build/
65+
66+
# PyBuilder
67+
target/
68+
69+
# IPython Notebook
70+
.ipynb_checkpoints
71+
72+
# pyenv
73+
.python-version
74+
75+
# celery beat schedule file
76+
celerybeat-schedule
77+
78+
# dotenv
79+
.env
80+
81+
# virtualenv
82+
venv/
83+
ENV/
84+
85+
# Spyder project settings
86+
.spyderproject
87+
88+
# Rope project settings
89+
.ropeproject
90+
91+
venv*

LICENSE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2021 Matthew Reid <[email protected]>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
5+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
6+
permit persons to whom the Software is furnished to do so, subject to the following conditions:
7+
8+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
11+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
13+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# tslumd
2+
3+
Client and Server for TSLUMD Tally Protocols
4+
5+
# License
6+
7+
Copyright (c) 2021 Matthew Reid <[email protected]>
8+
9+
tslumd is licensed under the MIT license, please see LICENSE file for details.

setup.cfg

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[bdist_wheel]
2+
universal = True
3+
4+
[metadata]
5+
name = tslumd
6+
version = 0.0.1
7+
author = Matthew Reid
8+
author_email = [email protected]
9+
url = https://github.com/nocarryr/tslumd
10+
description = Client and Server for TSLUMD Tally Protocols
11+
long_description = file: README.md
12+
long_description_content_type = text/markdown
13+
license = MIT
14+
license_file = LICENSE
15+
platforms = any
16+
python_requires = 3.7
17+
classifiers =
18+
Development Status :: 2 - Pre-Alpha
19+
Programming Language :: Python :: 3
20+
Programming Language :: Python :: 3.7
21+
Programming Language :: Python :: 3.8
22+
23+
24+
[options]
25+
package_dir=
26+
=src
27+
packages = find:
28+
install_requires =
29+
loguru
30+
python-dispatch
31+
32+
33+
[options.packages.find]
34+
where = src
35+
exclude = tests
36+
37+
38+
[options.package_data]
39+
* = LICENSE, README.md
40+
41+
[tool:pytest]
42+
testpaths = tests

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from setuptools import setup
2+
3+
setup()

src/tslumd/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Implementation of the `UMDv5.0 Protocol`_ by `TSL Products`_ for tally
2+
and other production display/control purposes.
3+
4+
.. _UMDv5.0 Protocol: https://tslproducts.com/media/1959/tsl-umd-protocol.pdf
5+
.. _TSL Products: https://tslproducts.com
6+
"""
7+
8+
from .common import *
9+
from .messages import *
10+
from .receiver import *
11+
from .sender import *

src/tslumd/animated_sender.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
from loguru import logger
2+
import asyncio
3+
import socket
4+
import string
5+
import argparse
6+
import enum
7+
from typing import List, Dict, Tuple, Sequence, Iterable
8+
9+
from pydispatch import Dispatcher, Property, DictProperty, ListProperty
10+
11+
from tslumd import TallyColor, TallyType, Tally, UmdSender
12+
from tslumd.utils import logger_catch
13+
from tslumd.sender import ClientArgAction
14+
15+
16+
class AnimateMode(enum.Enum):
17+
vertical = 1
18+
horizontal = 2
19+
20+
class TallyTypeGroup:
21+
tally_type: TallyType
22+
num_tallies: int
23+
tally_colors: List[TallyColor]
24+
def __init__(self, tally_type: TallyType, num_tallies: int):
25+
if tally_type == TallyType.no_tally:
26+
raise ValueError(f'TallyType cannot be {TallyType.no_tally}')
27+
self.tally_type = tally_type
28+
self.num_tallies = num_tallies
29+
self.tally_colors = [TallyColor.OFF for _ in range(num_tallies)]
30+
31+
def reset_all(self, color: TallyColor = TallyColor.OFF):
32+
self.tally_colors[:] = [color for _ in range(self.num_tallies)]
33+
34+
def update_tallies(self, tallies: Iterable[Tally]) -> List[int]:
35+
attr = self.tally_type.name
36+
changed = []
37+
for tally in tallies:
38+
color = self.tally_colors[tally.index]
39+
cur_value = getattr(tally, attr)
40+
if cur_value == color:
41+
continue
42+
setattr(tally, attr, color)
43+
changed.append(tally.index)
44+
return changed
45+
46+
class AnimatedSender(UmdSender):
47+
tally_groups: Dict[TallyType, TallyTypeGroup]
48+
num_tallies = 8
49+
update_interval = 1
50+
def __init__(self, clients=None):
51+
super().__init__(clients)
52+
for i in range(self.num_tallies):
53+
self.add_tally(i, text=string.ascii_uppercase[i])
54+
55+
self.tally_groups = {}
56+
for tally_type in TallyType:
57+
if tally_type == TallyType.no_tally:
58+
continue
59+
tg = TallyTypeGroup(tally_type, self.num_tallies)
60+
self.tally_groups[tally_type] = tg
61+
62+
async def open(self):
63+
if self.running:
64+
return
65+
await super().open()
66+
self.update_task = asyncio.create_task(self.update_loop())
67+
68+
async def close(self):
69+
if not self.running:
70+
return
71+
# self.running = False
72+
self.update_task.cancel()
73+
try:
74+
await self.update_task
75+
except asyncio.CancelledError:
76+
pass
77+
self.update_task = None
78+
await super().close()
79+
80+
def set_animate_mode(self, mode: AnimateMode):
81+
self.animate_mode = mode
82+
if mode == AnimateMode.vertical:
83+
self.cur_group = TallyType.rh_tally
84+
self.cur_index = -2
85+
elif mode == AnimateMode.horizontal:
86+
self.cur_index = 0
87+
self.cur_group = TallyType.no_tally
88+
for tg in self.tally_groups.values():
89+
tg.reset_all()
90+
91+
def animate_tallies(self):
92+
if self.animate_mode == AnimateMode.vertical:
93+
self.animate_vertical()
94+
elif self.animate_mode == AnimateMode.horizontal:
95+
self.animate_horizontal()
96+
97+
def animate_vertical(self):
98+
colors = [c for c in TallyColor if c != TallyColor.OFF]
99+
100+
tg = self.tally_groups[self.cur_group]
101+
start_ix = self.cur_index
102+
tg.reset_all()
103+
104+
for color in colors:
105+
ix = start_ix + color.value-1
106+
if 0 <= ix < self.num_tallies:
107+
tg.tally_colors[ix] = color
108+
start_ix += 1
109+
110+
if start_ix > self.num_tallies:
111+
self.cur_index = -2
112+
if self.cur_group == TallyType.rh_tally:
113+
self.cur_group = TallyType.txt_tally
114+
elif self.cur_group == TallyType.txt_tally:
115+
self.cur_group = TallyType.lh_tally
116+
else:
117+
self.set_animate_mode(AnimateMode.horizontal)
118+
else:
119+
self.cur_index = start_ix
120+
121+
def animate_horizontal(self):
122+
tally_types = [t for t in TallyType]
123+
while tally_types[0] != self.cur_group:
124+
t = tally_types.pop(0)
125+
tally_types.append(t)
126+
for i, t in enumerate(tally_types):
127+
if t == TallyType.no_tally:
128+
continue
129+
tg = self.tally_groups[t]
130+
tg.reset_all()
131+
try:
132+
color = TallyColor(i+1)
133+
except ValueError:
134+
color = TallyColor.OFF
135+
tg.tally_colors[self.cur_index] = color
136+
try:
137+
t = TallyType(self.cur_group.value+1)
138+
self.cur_group = t
139+
except ValueError:
140+
self.cur_index += 1
141+
self.cur_group = TallyType.no_tally
142+
if self.cur_index >= self.num_tallies:
143+
self.set_animate_mode(AnimateMode.vertical)
144+
145+
@logger_catch
146+
async def update_loop(self):
147+
self.set_animate_mode(AnimateMode.vertical)
148+
149+
def update_tallies():
150+
changed = set()
151+
for tg in self.tally_groups.values():
152+
_changed = tg.update_tallies(self.tallies.values())
153+
changed |= set(_changed)
154+
return changed
155+
156+
await self.connected_evt.wait()
157+
158+
while self.running:
159+
await asyncio.sleep(self.update_interval)
160+
if not self.running:
161+
break
162+
self.animate_tallies()
163+
changed_ix = update_tallies()
164+
# changed_tallies = [self.tallies[i] for i in changed_ix]
165+
# await self.update_queue.put(changed_tallies)
166+
167+
168+
def main():
169+
p = argparse.ArgumentParser()
170+
p.add_argument(
171+
'-c', '--client', dest='clients', action=ClientArgAction#, type=str,
172+
)
173+
args = p.parse_args()
174+
175+
logger.info(f'Sending to clients: {args.clients!r}')
176+
177+
loop = asyncio.get_event_loop()
178+
sender = AnimatedSender(clients=args.clients)
179+
180+
# async def run():
181+
# await sender.open()
182+
# await asyncio.sleep(10)
183+
# await sender.close()
184+
# try:
185+
# loop.run_until_complete(run())
186+
# finally:
187+
# loop.close()
188+
189+
loop.run_until_complete(sender.open())
190+
try:
191+
loop.run_forever()
192+
except KeyboardInterrupt:
193+
loop.run_until_complete(sender.close())
194+
finally:
195+
loop.close()
196+
197+
if __name__ == '__main__':
198+
main()

0 commit comments

Comments
 (0)