diff --git a/03_execution/animationFileReaders/AnimationFileReader.py b/03_execution/animationFileReaders/AnimationFileReader.py new file mode 100644 index 0000000..f597947 --- /dev/null +++ b/03_execution/animationFileReaders/AnimationFileReader.py @@ -0,0 +1,13 @@ +class AnimationFileReader(): + def __init__(self): + pass + + def nextFrame(self): + return false + + def getFrame(self): + return None + + def resetAnimation(self): + pass + diff --git a/03_execution/animationFileReaders/CsvAnimationFileReader.py b/03_execution/animationFileReaders/CsvAnimationFileReader.py new file mode 100644 index 0000000..1860f14 --- /dev/null +++ b/03_execution/animationFileReaders/CsvAnimationFileReader.py @@ -0,0 +1,34 @@ +from csv import reader +from animationFileReaders.AnimationFileReader import AnimationFileReader + + +class CsvAnimationFileReader(AnimationFileReader): + def __init__(self, filePath): + AnimationFileReader.__init__(self) + self._filePath = filePath + self._file = None + self._fileReader = None + self._frame = None + self.resetAnimation() + + def resetAnimation(self): + if (self._file != None): + self._file.close() + self._file = open(self._filePath, 'r') + self._fileReader = reader(self._file) + self.skipFrame() + self.nextFrame() + + def skipFrame(self): + row = next(self._fileReader, None) + + def nextFrame(self): + row = next(self._fileReader, None) + if (row == None): return False + row.pop(0) + self._frame = [(float(row[pixelNumber]), float(row[pixelNumber+1]), float(row[pixelNumber+2])) for pixelNumber in range(0, len(row), 3)] + return True + + def getFrame(self): + return self._frame + diff --git a/03_execution/animationFileReaders/__init__.py b/03_execution/animationFileReaders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/03_execution/ledsAdapters/LedsAdapter.py b/03_execution/ledsAdapters/LedsAdapter.py new file mode 100644 index 0000000..3515026 --- /dev/null +++ b/03_execution/ledsAdapters/LedsAdapter.py @@ -0,0 +1,9 @@ +class LedsAdapter(): + def __init__(self, ledsCount): + self._ledsCount = ledsCount + + def showFrame(self, frame): + pass + + def flush(self): + self.showFrame([(0, 0, 0) for x in range(self._ledsCount)]) diff --git a/03_execution/ledsAdapters/PhysicalLedsAdapter.py b/03_execution/ledsAdapters/PhysicalLedsAdapter.py new file mode 100644 index 0000000..6f2c73d --- /dev/null +++ b/03_execution/ledsAdapters/PhysicalLedsAdapter.py @@ -0,0 +1,14 @@ +import board +import neopixel +from ledsAdapters.LedsAdapter import LedsAdapter + +class PhysicalLedsAdapter(LedsAdapter): + def __init__(self, ledsCount): + LedsAdapter.__init__(self, ledsCount) + self._pixels = neopixel.NeoPixel(board.D18, ledsCount, auto_write=False, pixel_order=neopixel.RGB) + + def showFrame(self, frame): + ledsCounter = 0 + for color in frame: + self.pixels[ledsCounter] = color + ledsCounter += 1 diff --git a/03_execution/ledsAdapters/VisualLedsAdapter.py b/03_execution/ledsAdapters/VisualLedsAdapter.py new file mode 100644 index 0000000..a87c9f9 --- /dev/null +++ b/03_execution/ledsAdapters/VisualLedsAdapter.py @@ -0,0 +1,104 @@ +import threading +import OpenGL +from OpenGL.GL import * +from OpenGL.GLUT import * +from OpenGL.GLU import * + +from ledsMapReaders.LedsMapReader import LedsMapReader +from ledsAdapters.LedsAdapter import LedsAdapter + +import math + + +class VisualLedsAdapter(LedsAdapter): + def __init__(self, ledsCount, ledsMapReader, w, h): + LedsAdapter.__init__(self, ledsCount) + self._leds = [(0, 0, 0) for i in range(ledsCount)] + self._ledsMapReader = ledsMapReader + self._thread = threading.Thread(target=self.startOpenGlThread, args=()) + self._thread.start() + self._cameraRotation = 0 + self._cameraRotationX = 0 + self._cameraZoom = 1 + self._bounds = (w, h) + + def startOpenGlThread(self): + glutInit() + glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE) + glutInitWindowSize(self._bounds[0], self._bounds[1]) + glutInitWindowPosition(0, 0) + wind = glutCreateWindow("OpenGL tree visualization") + glutDisplayFunc(self.paint) + glutIdleFunc(self.paint) + glEnable(GL_DEPTH_TEST) + glDepthFunc(GL_LESS); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glutMotionFunc(self.rotateTree) + glutMainLoop() # Keeps the window created above displaying/running in a loop + + def rotateTree(self, x, y): + self._cameraRotation = 360*(x - self._bounds[0]/2)/self._bounds[0] + self._cameraRotationX = -360*(y - self._bounds[1]/2)/self._bounds[1] + + def paint(self): + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # Remove everything from screen (i.e. displays all white) + + glMatrixMode(GL_PROJECTION); + glLoadIdentity() # Reset all graphic/shape's position + glFrustum(-2.0, 2.0, -0.5, 3.5, 6.0, 10.0) + glTranslate(0.0, 1.0, -8.0) + glRotate(-self._cameraRotationX, 1.0, 0.0, 0.0) + glRotate(-self._cameraRotation, 0.0, 1.0, 0.0) + glTranslate(0.0, -1.0, 0.0) + self.drawTree() + self.drawLights() + glTranslate(0.0, 1.0, 0.0) + glRotate(self._cameraRotation, 0.0, 1.0, 0.0) + glRotate(self._cameraRotationX, 1.0, 0.0, 0.0) + glTranslate(0.0, -1.0, 8.0) + glMatrixMode(GL_MODELVIEW) + glutSwapBuffers() + + def drawTree(self): + topLedCoords = [0.0, 0.0, 0.0] + for ledIndex in range(0, self._ledsCount): + led = self._ledsMapReader.getLed(ledIndex) + if led[1] >= topLedCoords[1]: + topLedCoords = led + + glTranslate(topLedCoords[0], 0, topLedCoords[2]) + + glColor4f(0.3, 0.2, 0.0, 1.0) + glRotate(-90, 1.0, 0.0, 0.0) + glutSolidCone(0.1, 3.2, 10, 10) + glRotate(90, 1.0, 0.0, 0.0) + + glColor4f(0.1, 0.8, 0.1, 0.15) + for hInt in range(17): + h = float(hInt)/5 + glTranslate(0, h, 0) + glRotate((hInt/16)*70, 0.0, 1.0, 0.0) + glRotate(90, 1.0, 0.0, 0.0) + glutWireCone(1.2 - 0.5/(3.6 - h), h/6, 50, 1) + glRotate(-90, 1.0, 0.0, 0.0) + glRotate(-(hInt/16)*70, 0.0, 1.0, 0.0) + glTranslate(0, -h, 0) + + glTranslate(-topLedCoords[0], 0, -topLedCoords[2]) + + + def drawLights(self): + for ledIndex in range(0, self._ledsCount): + color = [float(self._leds[ledIndex][0])/255, float(self._leds[ledIndex][1])/255, float(self._leds[ledIndex][2])/255] + ledCoords = self._ledsMapReader.getLed(ledIndex) + glTranslate(ledCoords[0]*self._cameraZoom, ledCoords[1]*self._cameraZoom, ledCoords[2]*self._cameraZoom) + glColor3f(color[0], color[1], color[2]) + gluSphere(gluNewQuadric(), 0.02, 10, 10) + glColor4f(color[0], color[1], color[2], 0.8) + gluSphere(gluNewQuadric(), 0.03, 10, 10) + glTranslate(-ledCoords[0]*self._cameraZoom, -ledCoords[1]*self._cameraZoom, -ledCoords[2]*self._cameraZoom) + + def showFrame(self, frame): + self._leds = frame diff --git a/03_execution/ledsAdapters/__init__.py b/03_execution/ledsAdapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/03_execution/ledsMapReaders/CsvLedsMapReader.py b/03_execution/ledsMapReaders/CsvLedsMapReader.py new file mode 100644 index 0000000..9d7a478 --- /dev/null +++ b/03_execution/ledsMapReaders/CsvLedsMapReader.py @@ -0,0 +1,15 @@ +from csv import reader +from ledsMapReaders.LedsMapReader import LedsMapReader + + +class CsvLedsMapReader(LedsMapReader): + def __init__(self, filePath): + LedsMapReader.__init__(self) + csvReader = reader(open(filePath, mode='r', encoding='utf-8-sig')) + for ledCoords in csvReader: + try: + float(ledCoords[0]) + except ValueError: + continue + if (len(ledCoords) == 4): ledCoords.pop(0) + self._leds.append([float(ledCoords[0]), float(ledCoords[2]), float(ledCoords[1])]) diff --git a/03_execution/ledsMapReaders/LedsMapReader.py b/03_execution/ledsMapReaders/LedsMapReader.py new file mode 100644 index 0000000..df9fc77 --- /dev/null +++ b/03_execution/ledsMapReaders/LedsMapReader.py @@ -0,0 +1,15 @@ +class LedsMapReader(): + def __init__(self): + self._leds = [] + + def normalize(self): + koef = 0 + for led in self._leds: + if koef < abs(led[0]): koef = abs(led[0]) + if koef < abs(led[2]): koef = abs(led[2]) + + for i in range(len(self._leds)): + self._leds[i] = [self._leds[i][0]/koef, self._leds[i][1]/koef, self._leds[i][2]/koef] + + def getLed(self, index): + return self._leds[index] diff --git a/03_execution/ledsMapReaders/TxtLedsMapReader.py b/03_execution/ledsMapReaders/TxtLedsMapReader.py new file mode 100644 index 0000000..65af21f --- /dev/null +++ b/03_execution/ledsMapReaders/TxtLedsMapReader.py @@ -0,0 +1,11 @@ +import json +from ledsMapReaders.LedsMapReader import LedsMapReader + + +class TxtLedsMapReader(LedsMapReader): + def __init__(self, filePath): + LedsMapReader.__init__(self) + ledsData = open(filePath, mode='r', encoding='utf-8-sig') + for ledRow in ledsData: + ledCoords = json.loads(ledRow) + self._leds.append([float(ledCoords[0]), float(ledCoords[2]), float(ledCoords[1])]) diff --git a/03_execution/ledsMapReaders/__init__.py b/03_execution/ledsMapReaders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/03_execution/run.py b/03_execution/run.py index bda3e18..5b9ce24 100644 --- a/03_execution/run.py +++ b/03_execution/run.py @@ -1,73 +1,36 @@ # Based on code from https://github.com/standupmaths/xmastree2020 -import board -import neopixel -import time -from csv import reader +import time import sys -# helper function for chunking -def chunks(lst, n): - for i in range(0, len(lst), n): - yield lst[i:i+n] +from animationFileReaders.CsvAnimationFileReader import CsvAnimationFileReader +from ledsAdapters.PhysicalLedsAdapter import PhysicalLedsAdapter -# sleep_time = 0.033 # approx 30fps -sleep_time = 0.017 # approx 60fps -NUMBEROFLEDS = 500 -pixels = neopixel.NeoPixel(board.D18, NUMBEROFLEDS, auto_write=False, pixel_order=neopixel.RGB) +class Tree(): + def __init__(self, ledsAdapter): + self._ledsAdapter = ledsAdapter -csvFile = sys.argv[1] + def runRepeatedAnimation(self, animationFileReader, repeats = 0, frameRate = 60): + repeatCounter = 0; + while (repeatCounter < repeats) or (repeats == 0): + animationFileReader.resetAnimation() + self.runAnimation(animationFileReader, frameRate) + repeatCounter += 1 + self.flushLeds() + def runAnimation(self, animationFileReader, frameRate = 60): + while True: + frame = animationFileReader.getFrame() + self._ledsAdapter.showFrame(frame) + time.sleep(1/frameRate) + if (not animationFileReader.nextFrame()): break -# read the file -# iterate through the entire thing and make all the points the same colour -lightArray = [] + def flushLeds(self): + self._ledsAdapter.flush() +mapReader.normalize() -with open(csvFile, 'r') as read_obj: - # pass the file object to reader() to get the reader object - csv_reader = reader(read_obj) - - # Iterate over each row in the csv using reader object - lineNumber = 0 - for row in csv_reader: - # row variable is a list that represents a row in csv - # break up the list of rgb values - # remove the first item - if lineNumber > 0: - parsed_row = [] - row.pop(0) - chunked_list = list(chunks(row, 3)) - for element_num in range(len(chunked_list)): - # this is a single light - r = float(chunked_list[element_num][0]) - g = float(chunked_list[element_num][1]) - b = float(chunked_list[element_num][2]) - light_val = (r, g, b) - # turn that led on - parsed_row.append(light_val) - - # append that line to lightArray - lightArray.append(parsed_row) - # time.sleep(0.03) - - lineNumber += 1 - -print("Finished Parsing") - - - -# run the code on the tree -while True: - f = 0 - for frame in lightArray: - print("running frame " + str(f)) - LED = 0 - while LED < NUMBEROFLEDS: - pixels[LED] = frame[LED] - LED += 1 - pixels.show() - - f += 1 -# time.sleep(sleep_time) +ledsAdapter = PhysicalLedsAdapter(500) +tree = Tree(ledsAdapter) +tree.runRepeatedAnimation(CsvAnimationFileReader(sys.argv[1]), 0, 60) diff --git a/03_execution/visualization.py b/03_execution/visualization.py new file mode 100644 index 0000000..2c661eb --- /dev/null +++ b/03_execution/visualization.py @@ -0,0 +1,49 @@ +# Based on code from https://github.com/standupmaths/xmastree2020 + +import time +import sys + +from animationFileReaders.CsvAnimationFileReader import CsvAnimationFileReader +from ledsAdapters.VisualLedsAdapter import VisualLedsAdapter +from ledsMapReaders.CsvLedsMapReader import CsvLedsMapReader +from ledsMapReaders.TxtLedsMapReader import TxtLedsMapReader + + +class Tree(): + def __init__(self, ledsAdapter): + self._ledsAdapter = ledsAdapter + + def runRepeatedAnimation(self, animationFileReader, repeats = 0, frameRate = 60): + repeatCounter = 0; + while (repeatCounter < repeats) or (repeats == 0): + animationFileReader.resetAnimation() + self.runAnimation(animationFileReader, frameRate) + repeatCounter += 1 + self.flushLeds() + + def runAnimation(self, animationFileReader, frameRate = 60): + while True: + frame = animationFileReader.getFrame() + self._ledsAdapter.showFrame(frame) + time.sleep(1/frameRate) + if (not animationFileReader.nextFrame()): break + + def flushLeds(self): + self._ledsAdapter.flush() + + +mapFileName = sys.argv[2] +mapFileNameExtension = mapFileName.split(".")[-1] +if (mapFileNameExtension == 'txt'): + mapReader = TxtLedsMapReader(mapFileName) +elif (mapFileNameExtension == 'csv'): + mapReader = CsvLedsMapReader(mapFileName) +else: + print('Unknown LED map type') + quit() + +mapReader.normalize() + +ledsAdapter = VisualLedsAdapter(500, mapReader, 800, 800) +tree = Tree(ledsAdapter) +tree.runRepeatedAnimation(CsvAnimationFileReader(sys.argv[1]), 0, 60) diff --git a/README.md b/README.md index f4c0bf9..3979a69 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ A few scripts, based on Matt's original code, are provided to load the CSV seque 4. If you stop a sequence and want to turn off the tree, run `$ sudo python3 flush.py` 5. If you need to fine calibrate the tree, you can turn on specific lights by ID running `$ sudo python3 turnon.py 0 99 199 299 399 499`. +## Visualization +If you haven't got real Cristmas tree with light, you still can visualize a tree from lights coordinates given in a csv file where each line represents a light position as "x,y,z" or "id,x,y,z", or in a txt file where each line also represents a light but the format of each line is "\[x, y, z\]" + +To run a sequence with OpenGL simulator execute this command: `$ sudo python3 visualization.py light-sequence.csv tree-map-file.csv` + +### Known Issues +There are some issues with running the visualization script on windows (problems with GLUT) + ## Contributions This repo is just a snapshot of the work we did in the Fall 2021 edition of the GSD-6338 course. Unfortunately, we do not have the resources to maintain, manage or extend it beyond what is available. If you want to ask questions, discuss standards, contribute new code, features and add new sexy goodness, please refer to [Matt's repo](https://github.com/standupmaths/xmastree2021) or start your own fork. Crediting is always welcome, thank you!