From f808b43573f4ef8611f28442662fa29f545cac82 Mon Sep 17 00:00:00 2001 From: Lord-Grey Date: Wed, 20 Nov 2024 21:03:41 +0100 Subject: [PATCH] Handle defined exits from python scripts --- effects/collision.py | 94 ++++++++++++++++++++------------- libsrc/python/PythonProgram.cpp | 58 ++++++++++++++++++-- 2 files changed, 112 insertions(+), 40 deletions(-) diff --git a/effects/collision.py b/effects/collision.py index 5c22fc83..09afa311 100644 --- a/effects/collision.py +++ b/effects/collision.py @@ -1,5 +1,3 @@ -# Two projectiles are sent from random positions and collide with each other -# Template from https://github.com/nickpesce/lit/blob/master/lit/effects/collision.py import hyperion, time, colorsys, random # Get parameters @@ -7,6 +5,11 @@ sleepTime = max(0.02, float(hyperion.args.get('speed', 100))/1000.0) trailLength = max(3, int(hyperion.args.get('trailLength', 5))) explodeRadius = int(hyperion.args.get('explodeRadius', 8)) +# Ensure that the range for pixel indices stays within bounds +maxPixelIndex = hyperion.ledCount - 1 +if trailLength > maxPixelIndex or explodeRadius > maxPixelIndex: + exit(f"Error: Color length ({trailLength}) and detonation range ({explodeRadius}) must be less than number of LEDs configured ({hyperion.ledCount})") + # Create additional variables increment = None projectiles = [] @@ -14,47 +17,64 @@ projectiles = [] # Initialize the led data ledData = bytearray() for i in range(hyperion.ledCount): - ledData += bytearray((0,0,0)) + ledData += bytearray((0,0,0)) # Start the write data loop while not hyperion.abort(): - if (len(projectiles) != 2): - projectiles = [ [0, 1, random.uniform(0.0, 1.0)], [hyperion.ledCount-1, -1, random.uniform(0.0, 1.0)] ] - increment = -random.randint(0, hyperion.ledCount-1) if random.choice([True, False]) else random.randint(0, hyperion.ledCount-1) + if len(projectiles) != 2: + projectiles = [ + [0, 1, random.uniform(0.0, 1.0)], # Start positions of projectiles + [hyperion.ledCount-1, -1, random.uniform(0.0, 1.0)] + ] + increment = -random.randint(0, hyperion.ledCount-1) if random.choice([True, False]) else random.randint(0, hyperion.ledCount-1) - ledDataBuf = ledData[:] - for i, v in enumerate(projectiles): - projectiles[i][0] = projectiles[i][0]+projectiles[i][1] - for t in range(0, trailLength): - pixel = v[0] - v[1]*t - if pixel + 2 < 0: - pixel += hyperion.ledCount - if pixel + 2 > hyperion.ledCount-1: - pixel -= hyperion.ledCount-1 - rgb = colorsys.hsv_to_rgb(v[2], 1, (trailLength - 1.0*t)/trailLength) - ledDataBuf[3*pixel ] = int(255*rgb[0]) - ledDataBuf[3*pixel + 1] = int(255*rgb[1]) - ledDataBuf[3*pixel + 2] = int(255*rgb[2]) + # Backup the LED data + ledDataBuf = ledData[:] + for i, v in enumerate(projectiles): + # Update projectile positions + projectiles[i][0] = projectiles[i][0] + projectiles[i][1] + + for t in range(0, trailLength): + # Calculate pixel index for the trail + pixel = v[0] - v[1] * t + if pixel < 0: + pixel += hyperion.ledCount + if pixel >= hyperion.ledCount: + pixel -= hyperion.ledCount - hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) + # Make sure pixel is within bounds + if pixel < 0 or pixel >= hyperion.ledCount: + continue - for i1, p1 in enumerate(projectiles): - for i2, p2 in enumerate(projectiles): - if (p1 is not p2): - prev1 = p1[0] - p1[1] - prev2 = p2[0] - p2[1] - if (prev1 - prev2 < 0) != (p1[0] - p2[0] < 0): - for d in range(0, explodeRadius): - for pixel in range(p1[0] - d, p1[0] + d): - rgb = colorsys.hsv_to_rgb(random.choice([p1[2], p2[2]]), 1, (1.0 * explodeRadius - d) / explodeRadius) - ledDataBuf[3*pixel ] = int(255*rgb[0]) - ledDataBuf[3*pixel + 1] = int(255*rgb[1]) - ledDataBuf[3*pixel + 2] = int(255*rgb[2]) + rgb = colorsys.hsv_to_rgb(v[2], 1, (trailLength - 1.0 * t) / trailLength) + ledDataBuf[3*pixel] = int(255 * rgb[0]) + ledDataBuf[3*pixel + 1] = int(255 * rgb[1]) + ledDataBuf[3*pixel + 2] = int(255 * rgb[2]) - hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) - time.sleep(sleepTime) + hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) - projectiles.remove(p1) - projectiles.remove(p2) + # Check for collision and handle explosion + for i1, p1 in enumerate(projectiles): + for i2, p2 in enumerate(projectiles): + if p1 is not p2: + prev1 = p1[0] - p1[1] + prev2 = p2[0] - p2[1] + if (prev1 - prev2 < 0) != (p1[0] - p2[0] < 0): + for d in range(0, explodeRadius): + for pixel in range(p1[0] - d, p1[0] + d): + # Check if pixel is out of bounds + if pixel < 0 or pixel >= hyperion.ledCount: + continue - time.sleep(sleepTime) + rgb = colorsys.hsv_to_rgb(random.choice([p1[2], p2[2]]), 1, (1.0 * explodeRadius - d) / explodeRadius) + ledDataBuf[3 * pixel] = int(255 * rgb[0]) + ledDataBuf[3 * pixel + 1] = int(255 * rgb[1]) + ledDataBuf[3 * pixel + 2] = int(255 * rgb[2]) + + hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) + time.sleep(sleepTime) + + projectiles.remove(p1) + projectiles.remove(p2) + + time.sleep(sleepTime) diff --git a/libsrc/python/PythonProgram.cpp b/libsrc/python/PythonProgram.cpp index 80574474..38a27934 100644 --- a/libsrc/python/PythonProgram.cpp +++ b/libsrc/python/PythonProgram.cpp @@ -102,16 +102,66 @@ void PythonProgram::execute(const QByteArray& python_code) { if (PyErr_Occurred()) { - Error(_log, "###### PYTHON EXCEPTION ######"); - Error(_log, "## In effect '%s'", QSTRING_CSTR(_name)); - PyObject* errorType = NULL, * errorValue = NULL, * errorTraceback = NULL; PyErr_Fetch(&errorType, &errorValue, &errorTraceback); PyErr_NormalizeException(&errorType, &errorValue, &errorTraceback); + // Check if the exception is a SystemExit + PyObject* systemExitType = PyExc_SystemExit; + bool isSystemExit = PyObject_IsInstance(errorValue, systemExitType); + + if (isSystemExit) + { + // Extract the exit argument + PyObject* exitArg = PyObject_GetAttrString(errorValue, "code"); + + if (exitArg) + { + QString logErrorText; + if (PyTuple_Check(exitArg)) { + PyObject* errorMessage = PyTuple_GetItem(exitArg, 0); // Borrowed reference + PyObject* exitCode = PyTuple_GetItem(exitArg, 1); // Borrowed reference + + if (exitCode && PyLong_Check(exitCode)) + { + logErrorText = QString("[%1]: ").arg(PyLong_AsLong(exitCode)); + } + + if (errorMessage && PyUnicode_Check(errorMessage)) { + logErrorText.append(PyUnicode_AsUTF8(errorMessage)); + } + } + else if (PyUnicode_Check(exitArg)) { + // If the code is just a string, treat it as an error message + logErrorText.append(PyUnicode_AsUTF8(exitArg)); + } + else if (PyLong_Check(exitArg)) { + // If the code is just an integer, treat it as an exit code + logErrorText = QString("[%1]").arg(PyLong_AsLong(exitArg)); + } + + Error(_log, "Effect '%s' failed with error %s", QSTRING_CSTR(_name), QSTRING_CSTR(logErrorText)); + + Py_DECREF(exitArg); // Release the reference + } + else + { + Debug(_log, "No 'code' attribute found on SystemExit exception."); + } + // Clear the error so it won't propagate + PyErr_Clear(); + + Py_DECREF(systemExitType); + return; + } + Py_DECREF(systemExitType); + if (errorValue) { + Error(_log, "###### PYTHON EXCEPTION ######"); + Error(_log, "## In effect '%s'", QSTRING_CSTR(_name)); + QString message; if (PyObject_HasAttrString(errorValue, "__class__")) { @@ -166,6 +216,8 @@ void PythonProgram::execute(const QByteArray& python_code) } Error(_log, "###### EXCEPTION END ######"); } + // Clear the error so it won't propagate + PyErr_Clear(); } else {