Skip to content

Commit 2728d01

Browse files
committed
Initial Commit
1 parent ffe9953 commit 2728d01

8 files changed

Lines changed: 729 additions & 1 deletion

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ __pycache__/
66
# C extensions
77
*.so
88

9+
#custom docs & stuff
10+
Pyjector/
11+
Docs/
12+
tests/
13+
914
# Distribution / packaging
1015
.Python
1116
build/

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,41 @@
11
# CaveControl
2-
A simple Python GUI to control groups of projectors in a Cave
2+
One day in late 2020, upon returning to campus, I was disappointed to discover that the Crestron touchscreen that I use to control the 10 projectors in [The Marquette University Visualization Lab](https://www.eng.mu.edu/vizlab/) had died after months of neglect during the Covid-19 shutdown. Instead of replacing the unit, I decided to make a simple Python GUI to trigger the most common daily tasks of the Cave: powering on/off, opening/closing shutters, enabling/disabling stereoscopic 3D mode, and enabling/disabling the floor projectors.
3+
4+
![Screenshot](screenshot.png "CaveControl Screenshot")
5+
6+
The program is made in Python 3.9, and the GUI is made in TKinter with TTK. It is designed to be simple and cross-platform, and so it uses standard libraries exclusively and has no additional dependencies. "Build.bat" is an included shortcut that will build a standalone executable using PyInstaller, if desired.
7+
8+
In order to be useful for other Caves, some modification to the code will be required. The IPs and available commands are coded into christie.py. To discover additional commands or their parameters, consult the serial command API reference, ["M Series Serial API Commands Tech Ref.pdf"](https://www.christiedigital.com/globalassets/resources/public/020-100224-11-christie-lit-tech-ref-m-series-serial-commands.pdf), from Christie's website. The program is designed for use with Christie WU7K-M projectors, but should work similarly with other makes and models with slight adjustment to the commands.
9+
10+
Special Thanks to [jmusarra](https://github.com/jmusarra), whose [Pyjector](https://github.com/chrislarkee/Pyjector) was useful for emulating the projectors while working from home.
11+
12+
Written by Chris Larkee, 4/29/21.
13+
14+
## Instructions
15+
16+
### For the command line:
17+
- Type "python christie.py" to see a list of available commands.
18+
- Type "python christie.py on" , for example, to turn on all 10 projectors.
19+
20+
```
21+
$ python christie.py
22+
Valid Commands:
23+
on Turn on all projectors
24+
off Turn off all projects
25+
open Open shutters (unmute video)
26+
close Close shutters (mute video)
27+
floorOn Enable floor projectors
28+
floorOff Disable floor projectors
29+
3d Enable frame-packed stereo
30+
2d Disable stereo
31+
ping Query power state
32+
test Secret debug command
33+
```
34+
35+
### For the GUI:
36+
- Run "python caveControl.py" , or CaveControl.exe.
37+
- Click on a command's radio button to send the command to all 10 projectors. Status text will appear at the bottom when all tasks are complete.
38+
- Press ESCAPE to quit, or close the window.
39+
40+
## License
41+
[GNU GPL v3](LICENSE.md)

build.bat

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
REM This batch file is a record of how the executable was built.
2+
REM This requires PyInstaller, of course.
3+
python -OO -m PyInstaller -w -F -a -i cubeicon.ico caveControl.py

caveControl.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#a tool to remotely control a group of Christie WU7K-M projectors using their network interfaces. This is the GUI version.
2+
3+
import tkinter as tk
4+
from tkinter import ttk
5+
from christie import transmit
6+
7+
class App(tk.Frame):
8+
def __init__(self, master=None):
9+
super().__init__(master)
10+
self.master = master
11+
root.title('Marquette Visualization Lab Control')
12+
root.geometry("450x470")
13+
14+
s = ttk.Style()
15+
s.theme_use('vista')
16+
s.configure('Heading.TLabel', font=('Helvetica Bold', 14))
17+
#s.configure('TLabel', font=('Helvetica', 11))
18+
19+
def create_widgets(self):
20+
#header
21+
header = tk.Frame(root)
22+
ttk.Label(header, text='Marquette Visualization Lab Control', style='Heading.TLabel').pack()
23+
ttk.Label(header, text='Select a command').pack()
24+
ttk.Separator(header).pack(expand=True, fill='x', pady=10)
25+
header.pack(expand=False, fill='x', pady=10)
26+
27+
#main area layout
28+
f = tk.Frame(root)
29+
ttk.Label(f, text='Power:').grid(row=0, column=0)
30+
ttk.Radiobutton(f, text='On', variable=p_power, value="on").grid(row=0, column=1, sticky="W")
31+
ttk.Radiobutton(f, text='Off', variable=p_power, value="off").grid(row=0, column=2, sticky="W")
32+
33+
ttk.Label(f, text='Shutters:').grid(row=1, column=0)
34+
ttk.Radiobutton(f, text='Open', variable=p_shutters, value="open").grid(row=1, column=1, sticky="W")
35+
ttk.Radiobutton(f, text='Closed', variable=p_shutters, value="close").grid(row=1, column=2, sticky="W")
36+
37+
ttk.Label(f, text='Stereo:').grid(row=2, column=0)
38+
ttk.Radiobutton(f, text='Frame Doubled', variable=p_3d, value="3d").grid(row=2, column=1, sticky="W")
39+
ttk.Radiobutton(f, text='Disabled', variable=p_3d, value="2d").grid(row=2, column=2, sticky="W")
40+
41+
ttk.Label(f, text='Floor:').grid(row=3, column=0)
42+
ttk.Radiobutton(f, text='Normal', variable=p_floor, value="floorOn").grid(row=3, column=1, sticky="W")
43+
ttk.Radiobutton(f, text='Disabled', variable=p_floor, value="floorOff").grid(row=3, column=2, sticky="W")
44+
45+
f.columnconfigure(0, pad=0, weight=2)
46+
f.columnconfigure(1, pad=0, weight=1)
47+
f.columnconfigure(2, pad=0, weight=1)
48+
f.rowconfigure(0, pad=20)
49+
f.rowconfigure(1, pad=20)
50+
f.rowconfigure(2, pad=20)
51+
f.rowconfigure(3, pad=20)
52+
f.pack(expand=False, fill='x', padx=10, pady=0)
53+
54+
#footer
55+
footer = tk.Frame(root)
56+
ttk.Separator(footer).pack(expand=True, fill='x', pady=5)
57+
ttk.Label(footer, textvariable=p_info).pack(pady=5)
58+
footer.pack(expand=False, fill='x', pady=0)
59+
60+
def onclick(s):
61+
p_info.set("Please wait...");
62+
app.update()
63+
p_info.set(transmit(s));
64+
65+
if __name__ == "__main__":
66+
root = tk.Tk()
67+
app = App(master=root)
68+
69+
#state variables & callbacks
70+
p_power = tk.StringVar()
71+
p_shutters = tk.StringVar()
72+
p_3d = tk.StringVar()
73+
p_floor = tk.StringVar()
74+
p_command = tk.StringVar()
75+
p_info = tk.StringVar(value="")
76+
p_power.trace_add('write', lambda *args: p_command.set(p_power.get()))
77+
p_shutters.trace_add('write', lambda *args: p_command.set(p_shutters.get()))
78+
p_3d.trace_add('write', lambda *args: p_command.set(p_3d.get()))
79+
p_floor.trace_add('write', lambda *args: p_command.set(p_floor.get()))
80+
p_command.trace_add('write', lambda *args: onclick(p_command.get()))
81+
82+
#keyboard shortcuts
83+
root.bind('<Escape>', lambda x: root.destroy())
84+
85+
app.create_widgets()
86+
app.mainloop()

christie.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#a tool to remotely control a group of Christie WU7K-M projectors using their network interfaces. This is the command-line utility that works as a back-end to a GUI.
2+
3+
import socket
4+
from sys import exit
5+
from sys import argv
6+
7+
from threading import Thread, active_count
8+
#from time import sleep
9+
#import random
10+
11+
statusreport = ""
12+
13+
def transmit(request):
14+
#translate friendly commands into the API's language
15+
if request == "on":
16+
command = "($PWR1)"
17+
elif request == "off":
18+
command = "($PWR0)"
19+
elif request == "open":
20+
command = "($SHU0)"
21+
elif request == "close":
22+
command = "($SHU1)"
23+
elif request == "3d":
24+
command = "($TDM+MAIN2)"
25+
elif request == "2d":
26+
command = "($TDM+MAIN0)"
27+
elif request == "floorOn":
28+
command = "(9PWR1)(10PWR1)"
29+
elif request == "floorOff":
30+
command = "(9PWR0)(10PWR0)"
31+
elif request == "ping":
32+
command = "(#PWR?)"
33+
elif request == "test":
34+
command = "(#SHU?)"
35+
else:
36+
print("Command not found.")
37+
exit(127)
38+
39+
global statusreport
40+
statusreport = ""
41+
for projector in range(1, 11):
42+
thr = Thread(target=transmitThread, args=(projector, command))
43+
thr.start()
44+
45+
while active_count() > 1:
46+
sleep(0.5)
47+
statusreport += "Transmission complete."
48+
return statusreport
49+
50+
def transmitThread(id, command):
51+
global statusreport
52+
ip = "172.22.1.1" + str(id)
53+
#sleep(random.randrange(5,50)/10.0)
54+
#statusreport += str(id) + " is ok.\n"
55+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
56+
s.settimeout(10.0)
57+
s.connect((ip, 3002))
58+
s.send(command.encode())
59+
response = s.recv(1024)
60+
s.close()
61+
statusreport += str(id) + ": " + response.decode() + "\n"
62+
63+
def printHelp():
64+
print("Valid Commands:",\
65+
"on\t\tTurn on all projectors",\
66+
"off\t\tTurn off all projects",\
67+
"open\t\tOpen shutters (unmute video)",\
68+
"close\t\tClose shutters (mute video)",\
69+
"floorOn\tEnable floor projectors",\
70+
"floorOff\tDisable floor projectors",\
71+
"3d\t\tEnable frame-packed stereo",\
72+
"2d\t\tDisable stereo",
73+
"ping\t\tQuery power state",
74+
"test\t\tSecret debug command",sep='\n ')
75+
exit(0)
76+
77+
if __name__ == '__main__':
78+
if len(argv) == 1:
79+
printHelp()
80+
81+
#ok go
82+
print(transmit(argv[1]))
83+
exit(0)

cubeicon.ico

35.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)