Skip to content

Commit ee93af4

Browse files
committed
Added Animations page with functionality
1 parent 4a3e191 commit ee93af4

10 files changed

+315
-4
lines changed

AnimationHandler.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
import glob, subprocess, os
3+
4+
5+
6+
7+
class AnimationHandler(object):
8+
9+
def __init__(self):
10+
self.script_path = "animations/"
11+
self.scripts = self.lookup_animation_scripts()
12+
self.script_names = self.strip_script_names(self.scripts)
13+
self.active_script = None
14+
print("AnimationHandler loaded with {} available scripts.".format(len(self.scripts)))
15+
16+
def lookup_animation_scripts(self):
17+
PATH = self.script_path+"*.py"
18+
return glob.glob(PATH)
19+
20+
def start_animation_by_name(self, animation_name):
21+
self.end_active_script()
22+
self.active_script = subprocess.Popen(['python', os.path.join(
23+
self.script_path, animation_name+".py")])
24+
return True
25+
26+
def end_active_script(self):
27+
if self.active_script is not None:
28+
self.active_script.terminate()
29+
30+
def strip_script_names(self, scripts):
31+
return [os.path.splitext(os.path.basename(s))[0] for s in scripts if "opc.py" not in s]
32+

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ A JS / Python control interface for a fadecandy-driven 8x8 LED matrix
1010

1111
## TODO Brainstorming
1212

13-
- [ ] Connection status to fadecandy server / LED matrix
13+
- [x] Connection status to fadecandy server / LED matrix
1414
- [ ] Some default color palettes
1515
- [ ] Drawing by hold-and-pull
16-
- [ ] Activation of animations
16+
- [x] Activation of animations
17+
- [ ] Include Perlin Noise as animation
1718
- [ ] Selfmade animations by drawing multiple keyframes
1819
- [ ] Sliding text to matrix
1920
- [x] Optimize for mobile usage (on smartphone)
21+
- [ ] Flask Restart option (see [Stackoverflow](https://stackoverflow.com/questions/11329917/restart-python-script-from-within-itself))
2022

2123

2224
## Color Palette Sliders

animations/1.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Script 1.py started.")

animations/2.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Script 2.py started.")

animations/3.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Script 3.py started.")

animations/chase.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env python
2+
3+
# Light each LED in sequence, and repeat.
4+
5+
import opc, time
6+
7+
numLEDs = 64
8+
client = opc.Client('localhost:7890')
9+
10+
while True:
11+
for i in range(numLEDs):
12+
pixels = [ (0,0,0) ] * numLEDs
13+
pixels[i] = (255, 255, 255)
14+
client.put_pixels(pixels)
15+
time.sleep(0.01)

animations/every-other-white.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env python
2+
3+
# Open Pixel Control client: Every other light to solid white, others dark.
4+
5+
import opc, time
6+
7+
numPairs = 256
8+
client = opc.Client('localhost:7890')
9+
10+
black = [ (0,0,0), (0,0,0) ] * numPairs
11+
white = [ (255,255,255), (0,0,0) ] * numPairs
12+
13+
# Fade to white
14+
client.put_pixels(black)
15+
client.put_pixels(black)
16+
time.sleep(0.5)
17+
client.put_pixels(white)

animations/opc.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python
2+
3+
"""Python Client library for Open Pixel Control
4+
http://github.com/zestyping/openpixelcontrol
5+
6+
Sends pixel values to an Open Pixel Control server to be displayed.
7+
http://openpixelcontrol.org/
8+
9+
Recommended use:
10+
11+
import opc
12+
13+
# Create a client object
14+
client = opc.Client('localhost:7890')
15+
16+
# Test if it can connect (optional)
17+
if client.can_connect():
18+
print('connected to %s' % ADDRESS)
19+
else:
20+
# We could exit here, but instead let's just print a warning
21+
# and then keep trying to send pixels in case the server
22+
# appears later
23+
print('WARNING: could not connect to %s' % ADDRESS)
24+
25+
# Send pixels forever at 30 frames per second
26+
while True:
27+
my_pixels = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
28+
if client.put_pixels(my_pixels, channel=0):
29+
print('...')
30+
else:
31+
print('not connected')
32+
time.sleep(1/30.0)
33+
34+
"""
35+
36+
import socket
37+
import struct
38+
import sys
39+
40+
class Client(object):
41+
42+
def __init__(self, server_ip_port, long_connection=True, verbose=False):
43+
"""Create an OPC client object which sends pixels to an OPC server.
44+
45+
server_ip_port should be an ip:port or hostname:port as a single string.
46+
For example: '127.0.0.1:7890' or 'localhost:7890'
47+
48+
There are two connection modes:
49+
* In long connection mode, we try to maintain a single long-lived
50+
connection to the server. If that connection is lost we will try to
51+
create a new one whenever put_pixels is called. This mode is best
52+
when there's high latency or very high framerates.
53+
* In short connection mode, we open a connection when it's needed and
54+
close it immediately after. This means creating a connection for each
55+
call to put_pixels. Keeping the connection usually closed makes it
56+
possible for others to also connect to the server.
57+
58+
A connection is not established during __init__. To check if a
59+
connection will succeed, use can_connect().
60+
61+
If verbose is True, the client will print debugging info to the console.
62+
63+
"""
64+
self.verbose = verbose
65+
66+
self._long_connection = long_connection
67+
68+
self._ip, self._port = server_ip_port.split(':')
69+
self._port = int(self._port)
70+
71+
self._socket = None # will be None when we're not connected
72+
73+
def _debug(self, m):
74+
if self.verbose:
75+
print(' %s' % str(m))
76+
77+
def _ensure_connected(self):
78+
"""Set up a connection if one doesn't already exist.
79+
80+
Return True on success or False on failure.
81+
82+
"""
83+
if self._socket:
84+
self._debug('_ensure_connected: already connected, doing nothing')
85+
return True
86+
87+
try:
88+
self._debug('_ensure_connected: trying to connect...')
89+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
90+
self._socket.connect((self._ip, self._port))
91+
self._debug('_ensure_connected: ...success')
92+
return True
93+
except socket.error:
94+
self._debug('_ensure_connected: ...failure')
95+
self._socket = None
96+
return False
97+
98+
def disconnect(self):
99+
"""Drop the connection to the server, if there is one."""
100+
self._debug('disconnecting')
101+
if self._socket:
102+
self._socket.close()
103+
self._socket = None
104+
105+
def can_connect(self):
106+
"""Try to connect to the server.
107+
108+
Return True on success or False on failure.
109+
110+
If in long connection mode, this connection will be kept and re-used for
111+
subsequent put_pixels calls.
112+
113+
"""
114+
success = self._ensure_connected()
115+
if not self._long_connection:
116+
self.disconnect()
117+
return success
118+
119+
def put_pixels(self, pixels, channel=0):
120+
"""Send the list of pixel colors to the OPC server on the given channel.
121+
122+
channel: Which strand of lights to send the pixel colors to.
123+
Must be an int in the range 0-255 inclusive.
124+
0 is a special value which means "all channels".
125+
126+
pixels: A list of 3-tuples representing rgb colors.
127+
Each value in the tuple should be in the range 0-255 inclusive.
128+
For example: [(255, 255, 255), (0, 0, 0), (127, 0, 0)]
129+
Floats will be rounded down to integers.
130+
Values outside the legal range will be clamped.
131+
132+
Will establish a connection to the server as needed.
133+
134+
On successful transmission of pixels, return True.
135+
On failure (bad connection), return False.
136+
137+
The list of pixel colors will be applied to the LED string starting
138+
with the first LED. It's not possible to send a color just to one
139+
LED at a time (unless it's the first one).
140+
141+
"""
142+
self._debug('put_pixels: connecting')
143+
is_connected = self._ensure_connected()
144+
if not is_connected:
145+
self._debug('put_pixels: not connected. ignoring these pixels.')
146+
return False
147+
148+
# build OPC message
149+
len_hi_byte = int(len(pixels)*3 / 256)
150+
len_lo_byte = (len(pixels)*3) % 256
151+
command = 0 # set pixel colors from openpixelcontrol.org
152+
153+
header = struct.pack("BBBB", channel, command, len_hi_byte, len_lo_byte)
154+
155+
pieces = [ struct.pack( "BBB",
156+
min(255, max(0, int(g))),
157+
min(255, max(0, int(r))),
158+
min(255, max(0, int(b)))) for r, g, b in pixels ]
159+
160+
if sys.version_info[0] == 3:
161+
# bytes!
162+
message = header + b''.join(pieces)
163+
else:
164+
# strings!
165+
message = header + ''.join(pieces)
166+
167+
self._debug('put_pixels: sending pixels to server')
168+
try:
169+
self._socket.send(message)
170+
except socket.error:
171+
self._debug('put_pixels: connection lost. could not send pixels.')
172+
self._socket = None
173+
return False
174+
175+
if not self._long_connection:
176+
self._debug('put_pixels: disconnecting')
177+
self.disconnect()
178+
179+
return True
180+
181+

run.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from flask import jsonify, render_template, request, Flask
22
from Matrix import Matrix
3+
from AnimationHandler import AnimationHandler
34

45
app = Flask(__name__)
56
#app.config.from_object(__name__)
@@ -10,10 +11,33 @@
1011

1112

1213
@app.route('/', defaults={'site': 'draw'})
13-
@app.route('/<any(animations, draw, gradient, text):site>')
14+
@app.route('/<any(draw, gradient, text):site>')
1415
def index(site):
1516
return render_template('{0}.html'.format(site), site=site, connected = mat.can_connect())
1617

18+
ah = None
19+
20+
@app.route('/animations')
21+
def animations():
22+
global ah
23+
ah = AnimationHandler()
24+
return render_template('animations.html', site='animations', scripts=ah.script_names)
25+
26+
@app.route('/startAnimation', methods=['POST'])
27+
def startAnimation():
28+
s = request.form.get('script', 'Error in Ajax, see run.py', type=str)
29+
if ah is not None:
30+
ah.start_animation_by_name(s)
31+
32+
return jsonify(active_script = s)
33+
34+
@app.route('/stopAnimation', methods=['POST'])
35+
def stopAnimation():
36+
if ah is not None:
37+
ah.end_active_script()
38+
return jsonify(active_script = "None")
39+
40+
1741

1842
@app.route('/setPixel', methods=['POST'])
1943
def setPixel():

templates/animations.html

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
11
{% extends 'index.html'%}
22

33
{% block content %}
4-
<h1>Animations</h1>
4+
5+
<div class="card">
6+
<div class="card-header">Animations - Active: <span class="badge badge-primary" id="activeAnimation">None</span>
7+
<button type="button" class="btn btn-danger float-right" id="stopScript">Kill current animation</button>
8+
</div>
9+
<div class="card-body">
10+
{% for script in scripts%}
11+
<button type="button" class="btn btn-info btn-block" id="{{script}}">{{ script }}</button>
12+
{% endfor %}
13+
</div>
14+
</div>
15+
516
{% endblock %}
617

718

819
{% block script %}
20+
<script>
21+
{% for script in scripts%}
22+
$('#{{script}}').on('click', function(e) {
23+
$.ajax({
24+
method: 'POST',
25+
url: {{ url_for('startAnimation')|tojson }},
26+
data: {'script': '{{ script }}' },
27+
dataType: "json"
28+
}).done(function(data) {
29+
$("#activeAnimation").text(data.active_script);
30+
});
31+
32+
console.log("Starting python animation: {{script}}");
33+
});
34+
{% endfor %}
935

36+
$('#stopScript').on('click', function(e) {
37+
$.ajax({
38+
method: 'POST',
39+
url: {{ url_for('stopAnimation')|tojson }},
40+
data: {},
41+
dataType: "json"
42+
}).done(function(data) {
43+
$("#activeAnimation").text(data.active_script);
44+
});
45+
});
46+
</script>
1047
{% endblock %}

0 commit comments

Comments
 (0)