├── .gitignore ├── preview.gif ├── requirements.txt ├── .editorconfig ├── SerialCommands.py ├── SocketCommands.py ├── README.md ├── OrientationVisualization.py └── App.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env/ -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonwenner/python-orientation-visualization-app/HEAD/preview.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opengles==0.0.1 2 | pygame==2.0.1 3 | PyOpenGL==3.1.5 4 | pyserial==3.5 5 | websocket-server==0.4 6 | PySimpleGUI==4.45.0 7 | tk==0.1.0 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /SerialCommands.py: -------------------------------------------------------------------------------- 1 | import serial.tools.list_ports 2 | import serial 3 | 4 | class SerialCommands: 5 | def __init__(self, serial_speed): 6 | self.baud_rate = serial_speed 7 | self.serial = None 8 | 9 | @staticmethod 10 | def get_ports(): 11 | return list(serial.tools.list_ports.comports()) 12 | 13 | def connect(self, port): 14 | self.serial = serial.Serial(port, self.baud_rate) 15 | self.serial.flushInput() 16 | 17 | def is_connect(self): 18 | return self.serial.isOpen() 19 | 20 | def get_data(self): 21 | if not self.serial.isOpen(): 22 | return None 23 | else: 24 | return self.serial.readline().decode('utf-8').split(',') 25 | 26 | def disconnect(self): 27 | if self.serial is None: 28 | return 29 | self.serial.close() -------------------------------------------------------------------------------- /SocketCommands.py: -------------------------------------------------------------------------------- 1 | from websocket_server import WebsocketServer 2 | 3 | class SocketCommands: 4 | def __init__(self, host = '0.0.0.0', port = 8080): 5 | self.host = host 6 | self.port = port 7 | self.server = None 8 | self.data = None 9 | self.clients = [] 10 | 11 | def connect(self): 12 | self.server = WebsocketServer(self.port, self.host) 13 | self.server.set_fn_new_client(self.new_client) 14 | self.server.set_fn_client_left(self.client_left) 15 | self.server.set_fn_message_received(self.message_received) 16 | self.server.serve_forever() 17 | 18 | def new_client(self, client, server): 19 | current_client = str(client['id']) 20 | message = '[X] A new client connected of id: {}'.format(current_client) 21 | self.clients.append(message) 22 | 23 | def client_left(self, client, server): 24 | current_client = str(client['id']) 25 | message = '[X] Client disconnected of id: {}'.format(current_client) 26 | self.clients.append(message) 27 | 28 | def message_received(self, client, server, message): 29 | self.data = message 30 | 31 | def is_connect(self): 32 | return self.server is not None 33 | 34 | def get_data(self): 35 | if not self.data: 36 | return None 37 | else: 38 | return self.data.split(',') 39 | 40 | def disconnect(self): 41 | if self.server is None: return 42 | self.server.server_close() 43 | self.server = None 44 | self.data = None 45 | self.clients = [] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ORIENTATION VISUALIZATION 3 |

4 | 5 | ## :bulb: About 6 | The module that allows observing orientations through a 3D object from Euler angles or quaternion transmitted with WebSocket via wi-fi or serial port. 7 | 8 | ## :movie_camera: Preview 9 | 10 |
11 | 12 |
13 | 14 | ## :rocket: Technologies 15 | 16 | * [Python3](https://www.python.org/) 17 | * [OpenGL](https://pypi.org/project/PyOpenGL/) 18 | * [Pysimplegui](https://pysimplegui.readthedocs.io/en/latest/) 19 | 20 | ## :raised_hand: Warning 21 | To use this module, remember that data must be transmitted via serial port or WIFI in string where each data has to be separated by a comma. 22 | * Quaternion 23 | ```json 24 | "w,x,y,z" 25 | ``` 26 | * Data example 27 | ```json 28 | "00.0000000,00.0000000,00.0000000,00.0000000" 29 | ``` 30 | 31 | * Euler angles 32 | ```json 33 | "pitch,roll,yaw" 34 | ``` 35 | * Data example 36 | ```json 37 | "00.0000000,00.0000000,00.0000000" 38 | ``` 39 | 40 | ## :information_source: Getting Started 41 | 42 | 1. Fork this repository and clone it on your machine. 43 | 2. Change the directory to `python-orientation-visualization-app` where you cloned it. 44 | 45 | ## :zap: Module Getting Started 46 | 47 | 1. Install requirements. 48 | ```shell 49 | $ pi3 install -r requirements.txt 50 | ``` 51 | 2. Startup 52 | ```shell 53 | $ python3 App.py 54 | ``` 55 | * If you are going to use data transmission via Wifi, when connecting, keep in mind that the WebSocket server `IP` will be your machine's `IP` and port `8080`. 56 | --- 57 | Made with :hearts: by Nelson Wenner :wave: [Get in touch!](https://www.linkedin.com/in/nelsonwenner/) 58 | -------------------------------------------------------------------------------- /OrientationVisualization.py: -------------------------------------------------------------------------------- 1 | from pygame.locals import * 2 | from OpenGL.GLU import * 3 | from OpenGL.GL import * 4 | import pygame 5 | import math 6 | 7 | class OrientationVisualization: 8 | 9 | verticeA = [1.0, 0.2, 1.0] 10 | verticeB = [-1.0, 0.2, 1.0] 11 | verticeC = [-1.0, -0.2, 1.0] 12 | verticeD = [1.0, -0.2, 1.0] 13 | verticeE = [1.0, 0.2, -1.0] 14 | verticeF = [-1.0, 0.2, -1.0] 15 | verticeG = [-1.0, -0.2, -1.0] 16 | verticeH = [1.0, -0.2, -1.0] 17 | 18 | def start(self, device, app): 19 | pygame.init() 20 | flags = OPENGL | DOUBLEBUF 21 | screen = pygame.display.set_mode((1280, 720), flags) 22 | pygame.display.set_caption("Orientation Visualization") 23 | clock = pygame.time.Clock() 24 | self.screen(1280, 720) 25 | self.init() 26 | 27 | while True: 28 | if app.stop_thread_trigger: break 29 | data = device.get_data() 30 | self.display(data, app.current_type_data) 31 | pygame.display.flip() 32 | clock.tick(50) 33 | pygame.quit() 34 | 35 | def init(self): 36 | glShadeModel(GL_SMOOTH) 37 | glClearColor(0.0, 0.0, 0.0, 0.0) 38 | glClearDepth(1.0) 39 | glEnable(GL_DEPTH_TEST) 40 | glDepthFunc(GL_LEQUAL) 41 | glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) 42 | 43 | def screen(self, width, height): 44 | glViewport(0, 0, width, height) 45 | glMatrixMode(GL_PROJECTION) 46 | glLoadIdentity() 47 | gluPerspective(45, 1.0*width/height, 0.1, 100.0) 48 | glMatrixMode(GL_MODELVIEW) 49 | glLoadIdentity() 50 | 51 | def display(self, data, current_type_data): 52 | # Clear the image 53 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 54 | # Reset previous transforms 55 | glLoadIdentity() 56 | # Transform to perspective view 57 | glTranslatef(0, 0.0, -7.0) 58 | 59 | # Draw 60 | if current_type_data == '_QUATERNION_': 61 | w = float(data[0]) 62 | x = float(data[1]) 63 | y = float(data[2]) 64 | z = float(data[3]) 65 | self.draw(w, x, y, z, current_type_data) 66 | elif current_type_data == '_EULERANGLE_': 67 | yaw = float(data[0]) 68 | pitch = float(data[1]) 69 | roll = float(data[2]) 70 | self.draw(1, pitch, roll, yaw, current_type_data) 71 | 72 | self.draw_axes() 73 | 74 | # Flush and swap 75 | glFlush() 76 | 77 | def draw(self, w, x, y, z, current_type_data): 78 | 79 | if current_type_data == '_QUATERNION_': 80 | info = "Quaternion w: %.4f, x: %.4f, y: %.4f z: %.4f" %(w, x, y, z) 81 | self.draw_text((-2.6, -1.8, 2), info, 20) 82 | # W and the angle of rotation around the axis of the quaternion. 83 | # Specifies the angle of rotation, in degrees. 84 | angle = 2 * math.acos(w) * 180.00 / math.pi 85 | glRotatef(angle, x, z, y) 86 | 87 | elif current_type_data == '_EULERANGLE_': 88 | yaw, pitch, roll = x, y, z 89 | info = "Angle Euler Pitch: %f, Roll: %f, Yaw: %f" %(pitch, roll, yaw) 90 | self.draw_text((-2.6, -1.8, 2), info, 20) 91 | glRotatef(-roll, 0.00, 0.00, 1.00) 92 | glRotatef(pitch, 1.00, 0.00, 0.00) 93 | glRotatef(yaw, 0.00, 1.00, 0.00) 94 | 95 | glBegin(GL_QUADS) 96 | 97 | # FRONT: ABCD - GREEN 98 | glColor3f(0.0, 1.0, 0.0) 99 | glVertex3fv(self.verticeA) 100 | glVertex3fv(self.verticeB) 101 | glVertex3fv(self.verticeC) 102 | glVertex3fv(self.verticeD) 103 | 104 | # BACK: FEHG - GREEN 105 | glColor3f(0.0, 1.0, 0.0) 106 | glVertex3fv(self.verticeF) 107 | glVertex3fv(self.verticeE) 108 | glVertex3fv(self.verticeH) 109 | glVertex3fv(self.verticeG) 110 | 111 | # RIGHT: EADH - RED 112 | glColor3f(1.0, 0.0, 0.0) 113 | glVertex3fv(self.verticeE) 114 | glVertex3fv(self.verticeA) 115 | glVertex3fv(self.verticeD) 116 | glVertex3fv(self.verticeH) 117 | 118 | # LEFT: BFGC - RED 119 | glColor3f(1.0, 0.0, 0.0) 120 | glVertex3fv(self.verticeB) 121 | glVertex3fv(self.verticeF) 122 | glVertex3fv(self.verticeG) 123 | glVertex3fv(self.verticeC) 124 | 125 | # TOP: EFBA - BLUE 126 | glColor3f(0.0, 0.0, 1.0) 127 | glVertex3fv(self.verticeE) 128 | glVertex3fv(self.verticeF) 129 | glVertex3fv(self.verticeB) 130 | glVertex3fv(self.verticeA) 131 | 132 | # BOTTOM: DCGH - BLUE 133 | glColor3f(0.0, 0.0, 1.0) 134 | glVertex3fv(self.verticeD) 135 | glVertex3fv(self.verticeC) 136 | glVertex3fv(self.verticeG) 137 | glVertex3fv(self.verticeH) 138 | 139 | glEnd() 140 | 141 | def draw_axes(self, len = 2.0): 142 | glColor3f(1.0, 1.0, 1.0) 143 | glLineWidth(4.0) 144 | 145 | glBegin(GL_LINE_LOOP) 146 | glVertex3d(0, 0, 0) 147 | glVertex3d(len, 0, 0) 148 | glVertex3d(0, 0, 0) 149 | glVertex3d(0, len, 0) 150 | glVertex3d(0, 0, 0) 151 | glVertex3d(0, 0, len) 152 | glEnd() 153 | 154 | glVertex3d(len, 0, 0) 155 | self.draw_text((len, 0, 0), "X", 20) 156 | glVertex3d(0, len, 0) 157 | self.draw_text((0, len, 0), "Y", 20) 158 | glVertex3d(0, 0, len) 159 | self.draw_text((0, 0, len), "Z", 20) 160 | 161 | def draw_text(self, position, text, size): 162 | font = pygame.font.SysFont("Courier", size, True) 163 | textSurface = font.render(text, True, 164 | (255, 255, 255, 255), (0, 0, 0, 255)) 165 | textData = pygame.image.tostring(textSurface, "RGBA", True) 166 | glRasterPos3d(*position) 167 | glDrawPixels(textSurface.get_width(), textSurface.get_height(), 168 | GL_RGBA, GL_UNSIGNED_BYTE, textData 169 | ) 170 | -------------------------------------------------------------------------------- /App.py: -------------------------------------------------------------------------------- 1 | from OrientationVisualization import OrientationVisualization 2 | from SerialCommands import SerialCommands 3 | from SocketCommands import SocketCommands 4 | import PySimpleGUI as sg 5 | import threading 6 | 7 | class App: 8 | 9 | header_font = ('Courier', 14, 'bold') 10 | large_font = ('Courier', 12) 11 | medium_font = ('Courier', 10) 12 | small_font = ('Courier', 8) 13 | 14 | sg.SetOptions( 15 | background_color='#2C2C2C', 16 | text_element_background_color='#2C2C2C', 17 | element_background_color='#2C2C2C', 18 | scrollbar_color=None, 19 | input_elements_background_color='#FAFAFA', 20 | progress_meter_color=('#32D957', '#EEEEEE'), 21 | button_color=('#FAFAFA', '#222222') 22 | ) 23 | 24 | layout = [ 25 | [ 26 | sg.Text('Orientation Visualization', justification='center', 27 | pad=((28, 0), (10, 15)), font=header_font) 28 | ], 29 | [ 30 | sg.Checkbox('Wifi', key='_WIFI_', change_submits=True, 31 | font=large_font, pad=((72, 5), (0, 0))), 32 | sg.VerticalSeparator(), 33 | sg.Checkbox('Serial', key='_SERIAL_', change_submits=True, 34 | default=True, font=large_font, pad=((0, 0), (0, 0))) 35 | ], 36 | [ 37 | sg.Text('Select your serial port', pad=((68, 0), (20, 10)), 38 | key='_DEVICE_TITLE_', font=medium_font) 39 | ], 40 | [ 41 | sg.Listbox(values=[x[0] for x in SerialCommands.get_ports()], 42 | size=(40, 6), key='_DEVICE_LIST_', font=medium_font, enable_events=True) 43 | ], 44 | [ 45 | sg.Text('', key='_SERIAL_PORT_CONFIRM_', size=(40, 1), font=small_font) 46 | ], 47 | [ 48 | sg.HorizontalSeparator(pad=((0, 0), (0, 15))) 49 | ], 50 | [ 51 | sg.Checkbox('Quaternion', key='_QUATERNION_', change_submits=True, 52 | font=large_font, pad=((20, 5), (0, 0))), 53 | sg.VerticalSeparator(), 54 | sg.Checkbox('EulerAngle', key='_EULERANGLE_', change_submits=True, 55 | default=True, font=large_font, pad=((0, 0), (0, 0))) 56 | ], 57 | [ 58 | sg.Button('Start', key='_ACT_BUTTON_', font=medium_font, size=(40, 1), 59 | pad=((0, 0), (24, 0))) 60 | ], 61 | [ 62 | sg.Text('NelsonWenner - Version: 0.1', justification='right', size=(60, 1), 63 | pad=((0, 0), (10, 0)), font=small_font) 64 | ] 65 | ] 66 | 67 | def __init__(self): 68 | self.baud_rate = 115200 69 | self.current_device = '_SERIAL_' 70 | self.current_type_data = '_EULERANGLE_' 71 | self.stop_thread_trigger = False 72 | self.orientation_visualization = OrientationVisualization() 73 | self.serial_commands = SerialCommands(self.baud_rate) 74 | self.socket_commands = SocketCommands() 75 | self.window = sg.Window('', self.layout, size=(360, 400), keep_on_top=True) 76 | 77 | while True: 78 | event, values = self.window.Read(timeout=100) 79 | 80 | if event == sg.WIN_CLOSED: break 81 | 82 | if event == '_SERIAL_': 83 | self.window['_DEVICE_TITLE_'].update('Select your serial port') 84 | self.window['_SERIAL_'].update(True) 85 | self.window['_WIFI_'].update(False) 86 | self.current_device = '_SERIAL_' 87 | self.add_serial_port() 88 | self.socket_commands.disconnect() 89 | 90 | if event == '_WIFI_': 91 | self.window['_DEVICE_TITLE_'].update('Waiting for connections') 92 | self.window['_WIFI_'].update(True) 93 | self.window['_SERIAL_'].update(False) 94 | self.window['_DEVICE_LIST_'].update(values=['[X] Server listening in port 8080']) 95 | self.window['_SERIAL_PORT_CONFIRM_'].update(value='') 96 | self.current_device = '_WIFI_' 97 | self.thread_socket = threading.Thread( 98 | target=self.socket_commands.connect, 99 | daemon=True 100 | ) 101 | self.thread_socket.start() 102 | 103 | if event == '_QUATERNION_': 104 | self.window['_QUATERNION_'].update(True) 105 | self.window['_EULERANGLE_'].update(False) 106 | self.current_type_data = '_QUATERNION_' 107 | 108 | if event == '_EULERANGLE_': 109 | self.window['_EULERANGLE_'].update(True) 110 | self.window['_QUATERNION_'].update(False) 111 | self.current_type_data = '_EULERANGLE_' 112 | 113 | if self.current_device == '_WIFI_': 114 | if len(self.socket_commands.clients) != (len(self.window['_DEVICE_LIST_'].get()) - 1): 115 | info = ['[X] Server listening in port 8080'] + self.socket_commands.clients 116 | self.window['_DEVICE_LIST_'].update(values=info) 117 | 118 | if event == '_DEVICE_LIST_': 119 | current_values = self.window['_DEVICE_LIST_'].get()[0] 120 | self.window['_SERIAL_PORT_CONFIRM_'].update(value="[X] {}".format(current_values)) 121 | 122 | if event == '_ACT_BUTTON_': 123 | if self.window[event].get_text() == 'Start': 124 | if self.current_device == '_SERIAL_': 125 | if len(self.window['_DEVICE_LIST_'].get()) == 0: 126 | self.popup_dialog('Serial Port is not selected yet!', 'Serial Port', self.medium_font) 127 | elif self.serial_commands.get_data() is None: 128 | self.popup_dialog('The app is not receiving any data', 'Data Transmission', self.medium_font) 129 | elif self.current_type_data == '_QUATERNION_' and len(self.serial_commands.get_data()) != 4: 130 | self.popup_dialog('Invalid data type, quaternion requires 4 values (w, x, y, z)', 'Data Type', self.medium_font) 131 | elif self.current_type_data == '_EULERANGLE_' and len(self.serial_commands.get_data()) != 3: 132 | self.popup_dialog('Invalid data type, euler angle requires 3 values (x, y, z)', 'Data Type', self.medium_font) 133 | else: 134 | self.stop_thread_trigger = False 135 | self.thread_device = threading.Thread( 136 | target=self.start_orientation_visualization, 137 | args=( 138 | self.orientation_visualization, self.serial_commands, self, 139 | self.window['_DEVICE_LIST_'].get()[0] 140 | ), 141 | daemon=True 142 | ) 143 | self.thread_device.start() 144 | self.window['_ACT_BUTTON_'].update('Stop') 145 | 146 | elif self.current_device == '_WIFI_': 147 | if not self.socket_commands.clients: 148 | self.popup_dialog('Not have any device connected', 'Device Connection', self.medium_font) 149 | elif not ('new client' in self.socket_commands.clients[-1]): 150 | self.popup_dialog('Not have client connected', 'Client Connection', self.medium_font) 151 | elif self.socket_commands.get_data() is None: 152 | self.popup_dialog('The app is not receiving any data', 'Data Transmission', self.medium_font) 153 | elif self.current_type_data == '_QUATERNION_' and len(self.socket_commands.get_data()) != 4: 154 | self.popup_dialog('Invalid data type, quaternion requires 4 values (w, x, y, z)', 'Data Type', self.medium_font) 155 | elif self.current_type_data == '_EULERANGLE_' and len(self.socket_commands.get_data()) != 3: 156 | self.popup_dialog('Invalid data type, euler angle requires 3 values (x, y, z)', 'Data Type', self.medium_font) 157 | else: 158 | self.stop_thread_trigger = False 159 | self.thread_device = threading.Thread( 160 | target=self.start_orientation_visualization, 161 | args=( 162 | self.orientation_visualization, 163 | self.socket_commands, self, -1 164 | ), 165 | daemon=True 166 | ) 167 | self.thread_device.start() 168 | self.window['_ACT_BUTTON_'].update('Stop') 169 | else: 170 | self.stop_thread_trigger = True 171 | self.thread_device.join() 172 | self.window['_ACT_BUTTON_'].update('Start') 173 | 174 | self.window.close() 175 | 176 | def start_orientation_visualization(self, orientation_visualization, device, app, serialport): 177 | if self.current_device == '_SERIAL_': 178 | device.connect(serialport) 179 | if device.is_connect(): 180 | orientation_visualization.start(device, app) 181 | 182 | def add_serial_port(self): 183 | self.window['_DEVICE_LIST_'].update(values=[x[0] for x in SerialCommands.get_ports()]) 184 | 185 | def popup_dialog(self, contents, title, font): 186 | sg.Popup(contents, title=title, keep_on_top=True, font=font) 187 | 188 | if __name__ == '__main__': 189 | App() --------------------------------------------------------------------------------