Skip to content

Commit f808b43

Browse files
committed
Handle defined exits from python scripts
1 parent 07bf243 commit f808b43

File tree

2 files changed

+118
-46
lines changed

2 files changed

+118
-46
lines changed

effects/collision.py

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,80 @@
1-
# Two projectiles are sent from random positions and collide with each other
2-
# Template from https://github.com/nickpesce/lit/blob/master/lit/effects/collision.py
31
import hyperion, time, colorsys, random
42

53
# Get parameters
64
sleepTime = max(0.02, float(hyperion.args.get('speed', 100))/1000.0)
75
trailLength = max(3, int(hyperion.args.get('trailLength', 5)))
86
explodeRadius = int(hyperion.args.get('explodeRadius', 8))
97

8+
# Ensure that the range for pixel indices stays within bounds
9+
maxPixelIndex = hyperion.ledCount - 1
10+
if trailLength > maxPixelIndex or explodeRadius > maxPixelIndex:
11+
exit(f"Error: Color length ({trailLength}) and detonation range ({explodeRadius}) must be less than number of LEDs configured ({hyperion.ledCount})")
12+
1013
# Create additional variables
1114
increment = None
1215
projectiles = []
1316

1417
# Initialize the led data
1518
ledData = bytearray()
1619
for i in range(hyperion.ledCount):
17-
ledData += bytearray((0,0,0))
20+
ledData += bytearray((0,0,0))
1821

1922
# Start the write data loop
2023
while not hyperion.abort():
21-
if (len(projectiles) != 2):
22-
projectiles = [ [0, 1, random.uniform(0.0, 1.0)], [hyperion.ledCount-1, -1, random.uniform(0.0, 1.0)] ]
23-
increment = -random.randint(0, hyperion.ledCount-1) if random.choice([True, False]) else random.randint(0, hyperion.ledCount-1)
24-
25-
ledDataBuf = ledData[:]
26-
for i, v in enumerate(projectiles):
27-
projectiles[i][0] = projectiles[i][0]+projectiles[i][1]
28-
for t in range(0, trailLength):
29-
pixel = v[0] - v[1]*t
30-
if pixel + 2 < 0:
31-
pixel += hyperion.ledCount
32-
if pixel + 2 > hyperion.ledCount-1:
33-
pixel -= hyperion.ledCount-1
34-
rgb = colorsys.hsv_to_rgb(v[2], 1, (trailLength - 1.0*t)/trailLength)
35-
ledDataBuf[3*pixel ] = int(255*rgb[0])
36-
ledDataBuf[3*pixel + 1] = int(255*rgb[1])
37-
ledDataBuf[3*pixel + 2] = int(255*rgb[2])
38-
39-
hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment])
40-
41-
for i1, p1 in enumerate(projectiles):
42-
for i2, p2 in enumerate(projectiles):
43-
if (p1 is not p2):
44-
prev1 = p1[0] - p1[1]
45-
prev2 = p2[0] - p2[1]
46-
if (prev1 - prev2 < 0) != (p1[0] - p2[0] < 0):
47-
for d in range(0, explodeRadius):
48-
for pixel in range(p1[0] - d, p1[0] + d):
49-
rgb = colorsys.hsv_to_rgb(random.choice([p1[2], p2[2]]), 1, (1.0 * explodeRadius - d) / explodeRadius)
50-
ledDataBuf[3*pixel ] = int(255*rgb[0])
51-
ledDataBuf[3*pixel + 1] = int(255*rgb[1])
52-
ledDataBuf[3*pixel + 2] = int(255*rgb[2])
53-
54-
hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment])
55-
time.sleep(sleepTime)
56-
57-
projectiles.remove(p1)
58-
projectiles.remove(p2)
59-
60-
time.sleep(sleepTime)
24+
if len(projectiles) != 2:
25+
projectiles = [
26+
[0, 1, random.uniform(0.0, 1.0)], # Start positions of projectiles
27+
[hyperion.ledCount-1, -1, random.uniform(0.0, 1.0)]
28+
]
29+
increment = -random.randint(0, hyperion.ledCount-1) if random.choice([True, False]) else random.randint(0, hyperion.ledCount-1)
30+
31+
# Backup the LED data
32+
ledDataBuf = ledData[:]
33+
for i, v in enumerate(projectiles):
34+
# Update projectile positions
35+
projectiles[i][0] = projectiles[i][0] + projectiles[i][1]
36+
37+
for t in range(0, trailLength):
38+
# Calculate pixel index for the trail
39+
pixel = v[0] - v[1] * t
40+
if pixel < 0:
41+
pixel += hyperion.ledCount
42+
if pixel >= hyperion.ledCount:
43+
pixel -= hyperion.ledCount
44+
45+
# Make sure pixel is within bounds
46+
if pixel < 0 or pixel >= hyperion.ledCount:
47+
continue
48+
49+
rgb = colorsys.hsv_to_rgb(v[2], 1, (trailLength - 1.0 * t) / trailLength)
50+
ledDataBuf[3*pixel] = int(255 * rgb[0])
51+
ledDataBuf[3*pixel + 1] = int(255 * rgb[1])
52+
ledDataBuf[3*pixel + 2] = int(255 * rgb[2])
53+
54+
hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment])
55+
56+
# Check for collision and handle explosion
57+
for i1, p1 in enumerate(projectiles):
58+
for i2, p2 in enumerate(projectiles):
59+
if p1 is not p2:
60+
prev1 = p1[0] - p1[1]
61+
prev2 = p2[0] - p2[1]
62+
if (prev1 - prev2 < 0) != (p1[0] - p2[0] < 0):
63+
for d in range(0, explodeRadius):
64+
for pixel in range(p1[0] - d, p1[0] + d):
65+
# Check if pixel is out of bounds
66+
if pixel < 0 or pixel >= hyperion.ledCount:
67+
continue
68+
69+
rgb = colorsys.hsv_to_rgb(random.choice([p1[2], p2[2]]), 1, (1.0 * explodeRadius - d) / explodeRadius)
70+
ledDataBuf[3 * pixel] = int(255 * rgb[0])
71+
ledDataBuf[3 * pixel + 1] = int(255 * rgb[1])
72+
ledDataBuf[3 * pixel + 2] = int(255 * rgb[2])
73+
74+
hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment])
75+
time.sleep(sleepTime)
76+
77+
projectiles.remove(p1)
78+
projectiles.remove(p2)
79+
80+
time.sleep(sleepTime)

libsrc/python/PythonProgram.cpp

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,66 @@ void PythonProgram::execute(const QByteArray& python_code)
102102
{
103103
if (PyErr_Occurred())
104104
{
105-
Error(_log, "###### PYTHON EXCEPTION ######");
106-
Error(_log, "## In effect '%s'", QSTRING_CSTR(_name));
107-
108105
PyObject* errorType = NULL, * errorValue = NULL, * errorTraceback = NULL;
109106

110107
PyErr_Fetch(&errorType, &errorValue, &errorTraceback);
111108
PyErr_NormalizeException(&errorType, &errorValue, &errorTraceback);
112109

110+
// Check if the exception is a SystemExit
111+
PyObject* systemExitType = PyExc_SystemExit;
112+
bool isSystemExit = PyObject_IsInstance(errorValue, systemExitType);
113+
114+
if (isSystemExit)
115+
{
116+
// Extract the exit argument
117+
PyObject* exitArg = PyObject_GetAttrString(errorValue, "code");
118+
119+
if (exitArg)
120+
{
121+
QString logErrorText;
122+
if (PyTuple_Check(exitArg)) {
123+
PyObject* errorMessage = PyTuple_GetItem(exitArg, 0); // Borrowed reference
124+
PyObject* exitCode = PyTuple_GetItem(exitArg, 1); // Borrowed reference
125+
126+
if (exitCode && PyLong_Check(exitCode))
127+
{
128+
logErrorText = QString("[%1]: ").arg(PyLong_AsLong(exitCode));
129+
}
130+
131+
if (errorMessage && PyUnicode_Check(errorMessage)) {
132+
logErrorText.append(PyUnicode_AsUTF8(errorMessage));
133+
}
134+
}
135+
else if (PyUnicode_Check(exitArg)) {
136+
// If the code is just a string, treat it as an error message
137+
logErrorText.append(PyUnicode_AsUTF8(exitArg));
138+
}
139+
else if (PyLong_Check(exitArg)) {
140+
// If the code is just an integer, treat it as an exit code
141+
logErrorText = QString("[%1]").arg(PyLong_AsLong(exitArg));
142+
}
143+
144+
Error(_log, "Effect '%s' failed with error %s", QSTRING_CSTR(_name), QSTRING_CSTR(logErrorText));
145+
146+
Py_DECREF(exitArg); // Release the reference
147+
}
148+
else
149+
{
150+
Debug(_log, "No 'code' attribute found on SystemExit exception.");
151+
}
152+
// Clear the error so it won't propagate
153+
PyErr_Clear();
154+
155+
Py_DECREF(systemExitType);
156+
return;
157+
}
158+
Py_DECREF(systemExitType);
159+
113160
if (errorValue)
114161
{
162+
Error(_log, "###### PYTHON EXCEPTION ######");
163+
Error(_log, "## In effect '%s'", QSTRING_CSTR(_name));
164+
115165
QString message;
116166
if (PyObject_HasAttrString(errorValue, "__class__"))
117167
{
@@ -166,6 +216,8 @@ void PythonProgram::execute(const QByteArray& python_code)
166216
}
167217
Error(_log, "###### EXCEPTION END ######");
168218
}
219+
// Clear the error so it won't propagate
220+
PyErr_Clear();
169221
}
170222
else
171223
{

0 commit comments

Comments
 (0)