civicstream 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 VU-Civic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
File without changes
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: civicstream
3
+ Version: 1.0.0
4
+ Summary: CivicAlert Streaming Data Capture and Visualization Tool
5
+ Home-page: https://github.com/vu-civic/tools
6
+ Author: Will Hedgecock
7
+ Author-email: ronald.w.hedgecock@vanderbilt.edu
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/x-rst
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy
14
+ Requires-Dist: pyserial
15
+ Requires-Dist: pygame
16
+ Requires-Dist: PyOpenGL
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license-file
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ CivicStream
29
+ ===========
30
+
31
+ This package provides a command-line tool for capturing and visualizing streaming data
32
+ from a CivicAlert sensor device. It can be accessed from a command terminal by entering:
33
+
34
+ ``civicstream``
35
+
36
+ Enter ``civicstream -h`` to see a listing of available command line parameters, including
37
+ activation of an IMU visualizer or configuration of the number of incoming audio channels.
@@ -0,0 +1,10 @@
1
+ CivicStream
2
+ ===========
3
+
4
+ This package provides a command-line tool for capturing and visualizing streaming data
5
+ from a CivicAlert sensor device. It can be accessed from a command terminal by entering:
6
+
7
+ ``civicstream``
8
+
9
+ Enter ``civicstream -h`` to see a listing of available command line parameters, including
10
+ activation of an IMU visualizer or configuration of the number of incoming audio channels.
File without changes
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Required libraries and imports
5
+ from OpenGL.GL import *
6
+ from OpenGL.GLU import gluPerspective
7
+ from datetime import datetime
8
+ import itertools, math, struct, wave
9
+ import serial.tools.list_ports
10
+ import numpy as np
11
+ import argparse
12
+
13
+ # Hide the Pygame support prompt
14
+ import os
15
+ os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = 'True'
16
+ import pygame, pygame.locals
17
+
18
+ # Device constants
19
+ DEVICE_VID = 4617
20
+ DEVICE_PID = 2829
21
+ PACKET_HEADER_BYTES = 36
22
+ AUDIO_SAMPLE_WIDTH = 2
23
+ AUDIO_SAMPLE_RATE = 96000
24
+ AUDIO_BUFFER_SAMPLES = 32000
25
+ AUDIO_NUM_BYTES_PER_CHANNEL = AUDIO_SAMPLE_RATE * AUDIO_SAMPLE_WIDTH
26
+ AUDIO_BUFFER_CHUNKS = 4 * AUDIO_SAMPLE_RATE // AUDIO_BUFFER_SAMPLES
27
+ PACKET_END_DELIMITER = b"\xFE\xF0\xF2\x25"
28
+ RESPONSE_PACKET_DELIMITER = b"\xFE\xF9"
29
+ RESPONSE_ACK_PACKET = b"\x01\x02"
30
+
31
+ # Data packet structure
32
+ class DataPacket:
33
+ def __init__(self, audio, num_channels, timestamp, lat, lon, ht, q1, q2, q3, delimiter):
34
+ self.audio_num_bytes = num_channels * AUDIO_NUM_BYTES_PER_CHANNEL
35
+ self.audio = np.concatenate(np.frombuffer(audio, dtype=np.int16).reshape(AUDIO_BUFFER_CHUNKS, num_channels, -1), axis=1).tobytes()
36
+ self.timestamp = timestamp
37
+ self.lat = lat
38
+ self.lon = lon
39
+ self.ht = ht
40
+ self.qx = q2 / 1073741824.0
41
+ self.qy = q3 / 1073741824.0
42
+ self.qz = -q1 / 1073741824.0
43
+ self.qw = (1.0 - (self.qx**2 + self.qy**2 + self.qz**2))**0.5
44
+ self.roll, self.pitch, self.yaw = self.__quat_to_roll_pitch_yaw__()
45
+ self.delimiter = delimiter
46
+
47
+ @staticmethod
48
+ def _quat_mult(q1w, q1x, q1y, q1z, q2w, q2x, q2y, q2z):
49
+ return (q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z,
50
+ q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y,
51
+ q1w * q2y - q1x * q2z + q1y * q2w + q1z * q2x,
52
+ q1w * q2z + q1x * q2y - q1y * q2x + q1z * q2w)
53
+
54
+ @staticmethod
55
+ def _heading_to_direction(degrees):
56
+ if degrees > 330.0 or degrees <= 30.0:
57
+ return "N"
58
+ elif 30.0 < degrees <= 60.0:
59
+ return "NE"
60
+ elif 60.0 < degrees <= 120.0:
61
+ return "E"
62
+ elif 120.0 < degrees <= 150.0:
63
+ return "SE"
64
+ elif 150.0 < degrees <= 210.0:
65
+ return "S"
66
+ elif 210.0 < degrees <= 240.0:
67
+ return "SW"
68
+ elif 240.0 < degrees <= 300.0:
69
+ return "W"
70
+ elif 300.0 < degrees <= 330.0:
71
+ return "NW"
72
+
73
+ def __quat_to_roll_pitch_yaw__(self):
74
+ # If the quaternion is invalid, return zero angles
75
+ if isinstance(self.qw, complex):
76
+ return 0, 0, 0
77
+
78
+ # Compute roll (x-axis rotation)
79
+ t0 = self.qw * self.qx + self.qy * self.qz
80
+ t1 = 0.5 - (self.qx * self.qx + self.qy * self.qy)
81
+ roll = math.atan2(t0, t1) * 180.0 / math.pi
82
+
83
+ # Compute pitch (y-axis rotation)
84
+ t2 = 2.0 * (self.qw * self.qy - self.qx * self.qz)
85
+ t2 = max(-1.0, min(1.0, t2))
86
+ pitch = math.asin(t2) * 180.0 / math.pi
87
+
88
+ # Compute yaw (z-axis rotation)
89
+ t3 = self.qw * self.qz + self.qx * self.qy
90
+ t4 = 0.5 - (self.qy * self.qy + self.qz * self.qz)
91
+ yaw = math.atan2(t3, t4) * 180.0 / math.pi
92
+ #return roll, yaw, pitch
93
+ return -(90.0 + roll), pitch, (yaw - 90.0) % 360.0
94
+
95
+ def to_bytes(self):
96
+ return struct.pack(f"<{self.audio_num_bytes}sdfffiii4s", self.audio, self.timestamp, self.lat, self.lon, self.ht, self.q1, self.q2, self.q3, self.delimiter)
97
+
98
+ def __str__(self):
99
+ time_string = datetime.fromtimestamp(self.timestamp).strftime("%Y-%m-%d %H:%M:%S")
100
+ return f"\n------------------------- {time_string} -------------------------\n" \
101
+ f"Audio: {len(self.audio)} samples\n" \
102
+ f"Timestamp: {self.timestamp}\n" \
103
+ f"Location: <{self.lat}, {self.lon}, {self.ht}>\n" \
104
+ f"Orientation (Quaternions): <{self.qw}, {self.qx}, {self.qy}, {self.qz}>\n" \
105
+ f"Orientation (Roll/Pitch/Yaw): <{self.roll}, {self.pitch}, {self.yaw}>\n"
106
+
107
+ def __repr__(self):
108
+ return self.__str__()
109
+
110
+ @staticmethod
111
+ def from_bytes(data, num_channels):
112
+ audio_num_bytes = num_channels * AUDIO_NUM_BYTES_PER_CHANNEL
113
+ unpacked_data = struct.unpack("<dfffiii4s", data[audio_num_bytes:])
114
+ return DataPacket(data[:audio_num_bytes], num_channels, *unpacked_data)
115
+
116
+
117
+ # Visualizer class to handle IMU graphics rendering
118
+ class ImuVisualizer:
119
+ def __init__(self):
120
+ print("Initializing graphics libraries...")
121
+ pygame.init()
122
+ pygame.display.set_mode((640, 480), pygame.locals.OPENGL | pygame.locals.DOUBLEBUF)
123
+ pygame.display.set_caption("CivicAlert Orientation Visualizer")
124
+ glViewport(0, 0, 640, 480)
125
+ glMatrixMode(GL_PROJECTION)
126
+ glLoadIdentity()
127
+ gluPerspective(45, 1.0*640/480, 0.1, 100.0)
128
+ glMatrixMode(GL_MODELVIEW)
129
+ glLoadIdentity()
130
+ glShadeModel(GL_SMOOTH)
131
+ glClearColor(0.0, 0.0, 0.0, 0.0)
132
+ glClearDepth(1.0)
133
+ glEnable(GL_DEPTH_TEST)
134
+ glDepthFunc(GL_LEQUAL)
135
+ glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
136
+
137
+ def __draw_text__(self, position, textString, size):
138
+ font = pygame.font.SysFont("Courier", size, True)
139
+ textSurface = font.render(textString, True, (255, 255, 255, 255), (0, 0, 0, 255))
140
+ textData = pygame.image.tostring(textSurface, "RGBA", True)
141
+ glRasterPos3d(*position)
142
+ glDrawPixels(textSurface.get_width(), textSurface.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, textData)
143
+
144
+ def update(self, yaw, pitch, roll, qw, qx, qy, qz):
145
+ pygame.event.poll()
146
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
147
+ glLoadIdentity()
148
+ glTranslatef(0, 0.0, -7.0)
149
+ self.__draw_text__((-2.6, 1.8, 2), "CivicAlert Orientation", 18)
150
+ self.__draw_text__((-2.6, 1.6, 2), f"Heading: {int(yaw)}\N{DEGREE SIGN} ({DataPacket._heading_to_direction(yaw)}), Tilt: {int(pitch)}\N{DEGREE SIGN}", 16)
151
+ self.__draw_text__((-2.6, -1.9, 2), f"Yaw: {yaw:.2f}, Pitch: {pitch:.2f}, Roll: {roll:.2f}", 16)
152
+ glRotatef(2 * math.acos(qw) * 180.00 / math.pi, qz, qy, qx)
153
+
154
+ glBegin(GL_QUADS)
155
+ glColor3f(0.0, 1.0, 0.0)
156
+ glVertex3f(1.0, 0.2, -1.0)
157
+ glVertex3f(-1.0, 0.2, -1.0)
158
+ glVertex3f(-1.0, 0.2, 1.0)
159
+ glVertex3f(1.0, 0.2, 1.0)
160
+
161
+ glColor3f(1.0, 0.5, 0.0)
162
+ glVertex3f(1.0, -0.2, 1.0)
163
+ glVertex3f(-1.0, -0.2, 1.0)
164
+ glVertex3f(-1.0, -0.2, -1.0)
165
+ glVertex3f(1.0, -0.2, -1.0)
166
+
167
+ glColor3f(1.0, 0.0, 0.0)
168
+ glVertex3f(1.0, 0.2, 1.0)
169
+ glVertex3f(-1.0, 0.2, 1.0)
170
+ glVertex3f(-1.0, -0.2, 1.0)
171
+ glVertex3f(1.0, -0.2, 1.0)
172
+
173
+ glColor3f(1.0, 1.0, 0.0)
174
+ glVertex3f(1.0, -0.2, -1.0)
175
+ glVertex3f(-1.0, -0.2, -1.0)
176
+ glVertex3f(-1.0, 0.2, -1.0)
177
+ glVertex3f(1.0, 0.2, -1.0)
178
+
179
+ glColor3f(0.0, 0.0, 1.0)
180
+ glVertex3f(-1.0, 0.2, 1.0)
181
+ glVertex3f(-1.0, 0.2, -1.0)
182
+ glVertex3f(-1.0, -0.2, -1.0)
183
+ glVertex3f(-1.0, -0.2, 1.0)
184
+
185
+ glColor3f(1.0, 0.0, 1.0)
186
+ glVertex3f(1.0, 0.2, -1.0)
187
+ glVertex3f(1.0, 0.2, 1.0)
188
+ glVertex3f(1.0, -0.2, 1.0)
189
+ glVertex3f(1.0, -0.2, -1.0)
190
+ glEnd()
191
+ pygame.display.flip()
192
+
193
+
194
+ # Helper function to interleave non-interleaved audio data
195
+ def interleave_audio(audio_data, num_channels):
196
+ audio_num_bytes = num_channels * AUDIO_NUM_BYTES_PER_CHANNEL
197
+ ch_data = [np.frombuffer(audio_data[i:i+AUDIO_NUM_BYTES_PER_CHANNEL], dtype=np.int16)
198
+ for i in range(0, audio_num_bytes, AUDIO_NUM_BYTES_PER_CHANNEL)]
199
+ return np.array(list(itertools.chain(*zip(*ch_data)))).tobytes()
200
+
201
+
202
+ # Main program loop
203
+ def main_loop(visualize_imu, num_channels):
204
+
205
+ # Search for a CivicAlert device
206
+ device = None
207
+ for port in serial.tools.list_ports.comports():
208
+ if port.vid == DEVICE_VID and port.pid == DEVICE_PID:
209
+ print(f"CivicAlert device found on {port.device}")
210
+ device = port.device
211
+ break
212
+ if device is None:
213
+ print("CivicAlert device not found")
214
+ exit(1)
215
+
216
+ # Initialize the IMU visualizer if requested
217
+ imu_visualizer = ImuVisualizer() if visualize_imu else None
218
+
219
+ # Initiate a connection to the device
220
+ try:
221
+ print("Connecting to the CivicAlert device...")
222
+ with serial.Serial(device) as ser:
223
+ print("Successfully connected to the device!")
224
+ packet_size_bytes = PACKET_HEADER_BYTES + (num_channels * AUDIO_NUM_BYTES_PER_CHANNEL)
225
+
226
+ # Create a new wave file to save the audio data
227
+ now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
228
+ with wave.open(f"civicalert-{now}.wav", "wb") as wav_file:
229
+ wav_file.setnchannels(num_channels)
230
+ wav_file.setsampwidth(AUDIO_SAMPLE_WIDTH)
231
+ wav_file.setframerate(AUDIO_SAMPLE_RATE)
232
+
233
+ # Search for consecutive packet-ending delimiters
234
+ print("Awaiting packet synchronization...")
235
+ while True:
236
+ data = ser.read(2 * packet_size_bytes)
237
+ idx = data.find(PACKET_END_DELIMITER)
238
+ if idx > -1:
239
+ data = ser.read(idx + len(PACKET_END_DELIMITER))
240
+ if data[-len(PACKET_END_DELIMITER):] == PACKET_END_DELIMITER:
241
+ break
242
+ print("Packet synchronization complete! Starting data capture...")
243
+
244
+ # Loop forever reading data from the device
245
+ while True:
246
+
247
+ # Read the packet data
248
+ data = ser.read(packet_size_bytes)
249
+ ser.write(RESPONSE_PACKET_DELIMITER + RESPONSE_ACK_PACKET)
250
+
251
+ # Append the received audio to the WAV file and print the packet
252
+ packet = DataPacket.from_bytes(data, num_channels)
253
+ if packet.delimiter != PACKET_END_DELIMITER:
254
+ break
255
+ wav_file.writeframes(interleave_audio(packet.audio, num_channels))
256
+ if imu_visualizer:
257
+ imu_visualizer.update(packet.yaw, packet.pitch, packet.roll, packet.qw, packet.qx, packet.qy, packet.qz)
258
+ print(packet)
259
+
260
+ # Handle errors and exceptions
261
+ except serial.SerialException as e:
262
+ print(f"\nError communicating with the device: {e}")
263
+ print("\nExiting...")
264
+ except KeyboardInterrupt:
265
+ print("\nKeyboard interrupt detected. Exiting...")
266
+
267
+
268
+ # Application entry point
269
+ def main():
270
+
271
+ # Parse command line arguments and start the main loop
272
+ parser = argparse.ArgumentParser(description="CivicAlert Streaming Data Capture and Visualization Tool")
273
+ parser.add_argument("-i", "--imu", help="visualize streaming IMU data", default=False, action='store_true')
274
+ parser.add_argument("-c", "--channels", help="number of channels", default=1, type=int)
275
+ args = parser.parse_args()
276
+ main_loop(args.imu, args.channels)
277
+ pygame.quit()
278
+
279
+ if __name__ == '__main__':
280
+ main()
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: civicstream
3
+ Version: 1.0.0
4
+ Summary: CivicAlert Streaming Data Capture and Visualization Tool
5
+ Home-page: https://github.com/vu-civic/tools
6
+ Author: Will Hedgecock
7
+ Author-email: ronald.w.hedgecock@vanderbilt.edu
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/x-rst
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy
14
+ Requires-Dist: pyserial
15
+ Requires-Dist: pygame
16
+ Requires-Dist: PyOpenGL
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license-file
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ CivicStream
29
+ ===========
30
+
31
+ This package provides a command-line tool for capturing and visualizing streaming data
32
+ from a CivicAlert sensor device. It can be accessed from a command terminal by entering:
33
+
34
+ ``civicstream``
35
+
36
+ Enter ``civicstream -h`` to see a listing of available command line parameters, including
37
+ activation of an IMU visualizer or configuration of the number of incoming audio channels.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.rst
4
+ setup.py
5
+ app/__init__.py
6
+ app/civicstream.py
7
+ civicstream.egg-info/PKG-INFO
8
+ civicstream.egg-info/SOURCES.txt
9
+ civicstream.egg-info/dependency_links.txt
10
+ civicstream.egg-info/entry_points.txt
11
+ civicstream.egg-info/requires.txt
12
+ civicstream.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ civicstream = civicstream.civicstream:main
@@ -0,0 +1,4 @@
1
+ numpy
2
+ pyserial
3
+ pygame
4
+ PyOpenGL
@@ -0,0 +1 @@
1
+ civicstream
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ import setuptools
2
+
3
+ with open('README.rst', 'r') as fh:
4
+ long_description = fh.read()
5
+
6
+ setuptools.setup(
7
+ name='civicstream',
8
+ version='1.0.0',
9
+ author='Will Hedgecock',
10
+ author_email='ronald.w.hedgecock@vanderbilt.edu',
11
+ description='CivicAlert Streaming Data Capture and Visualization Tool',
12
+ long_description=long_description,
13
+ long_description_content_type='text/x-rst',
14
+ url='https://github.com/vu-civic/tools',
15
+ package_dir={'civicstream': 'app'},
16
+ packages=['civicstream'],
17
+ include_package_data=True,
18
+ install_requires=[
19
+ 'numpy',
20
+ 'pyserial',
21
+ 'pygame',
22
+ 'PyOpenGL'
23
+ ],
24
+ classifiers=[
25
+ 'Programming Language :: Python :: 3',
26
+ 'Operating System :: OS Independent',
27
+ ],
28
+ python_requires='>=3.8',
29
+ entry_points={
30
+ 'console_scripts': ['civicstream = civicstream.civicstream:main'],
31
+ }
32
+ )