├── .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()
--------------------------------------------------------------------------------