kneeAngleDataLoggerInterface.ipynb
Knee Angle Data Logger Interface
I developed a Windows 10 interface in Python for the knee angle data logger with a wired connection to a computer. The program may be run directly by launching kneeAngleDataLoggerInterface.pyw
or, naturally, by executing the following command.
$ python kneeAngleDataLoggerInterface.pyw
The interface prompts you to connect the knee angle data logger (via USB) if it has not already been connected, notifies that the device was connected, reads from the device over serial communication, and finally notifies that the device was disconnected (all Subsection 2). It then prepares the collected knee angle data (Subsection 3). Lastly, it makes the results accessible through a beautiful, interactive, in-browser plot (Subsection 4).
These features can be broken down by going through its library imports as follows.
1. Library Imports in Order of Appearance
The user is to at least be notified that the knee angle data logger was connected and disconnected (via USB).
win10toast
by Jithu Jacob is a Python library (PyPI, GitHub) for displaying Windows 10 toast notifications.
xxxxxxxxxx
from win10toast import ToastNotifier
On the other hand, plyer.notification
shows new notification senders for every notification sent.
For reasons that will become apparent, timer functionality is to be used.
time
is a standard Python library (Python documentation) for time access (and conversions, for that matter).
xxxxxxxxxx
from time import time
To read from the knee angle data logger, serial communication is to be used.
pySerial
by Chris Liechti is a Python library (PyPI, GitHub, documentation) that encapsulates access to computer serial ports, including emulated ones such as those created by USB.
xxxxxxxxxx
from serial import Serial
The received knee angle data is to be prepared before being made accessible.
xxxxxxxxxx
import numpy as np
import pandas as pd
pandas
, for instance, can be used to calculate a moving average to smooth the knee angle data.
The prepared knee angle data is to be made accessible through a beautiful, interactive, in-browser plot.
plotly
by the technical computing company of the same name is a Python library (PyPI, documentation) used to style interactive graphs.
xxxxxxxxxx
import plotly.graph_objects as go
2. Minimal User Interface | Reading from the Knee Angle Data Logger
To start with, initialize an instance of the toast notifier class using a memorable name:
xxxxxxxxxx
toaster = ToastNotifier()
This class has a show_toast
method which is to be used. Among other arguments, it accepts a notification title
, a notification msg
, and an optional boolean specifying whether or not the showing of the notification (in its entire duration) is to be threaded
(reference) with further Python instructions in this module (which calls show_toast
). I found that the notification message itself (not its title) is actually optional, being truly omitted by specifying msg
to be a non-empty ‘empty’ string such as ' '
. Method show_toast
returns a boolean representing whether a notification is sent successfully or not (i.e., if one is already being shown, at least from Python). I also found that initializing multiple instances of the ToastNotifier
class does not allow multiple corresponding notifications to appear simultaneously in the same way.
Now, specify the chosen 9600 / 8-N-1 serial communication (COM) port:
xxxxxxxxxx
port = 'COM4'
Try to open this serial port with the assumption that the knee angle data logger has been connected to the computer via USB:
xxxxxxxxxx
try:
│ ser = Serial(port)
If Serial
cannot open the specified port (i.e., if the above assumption was incorrect), it raises a SerialException
error, which is caught (handled) by the following to-be-completed block of code.
xxxxxxxxxx
except:
Now, I assume the general case that a system notification may already be present.
As such, keep trying to…
- show the user a prompt to connect to the knee angle data logger, or
- open the serial port assuming that the device has since been connected,
whichever happens first (that is, whichever the program encounters first):
xxxxxxxxxx
│ while True:
│ │
│ │ if toaster.show_toast('Connect to the knee angle data logger via USB', ' ',
│ │ │ threaded = True):
│ │ │ break
│ │
│ │ try:
│ │ │ ser = Serial(port)
│ │ │ break¹
│ │ except:
│ │ │ pass
¹ This break
will not be reached unless the previous line, ser = Serial(port)
, succeeds.
At least in this context, break
and pass
specifically mean ‘stop trying’ and ‘skip error handling’, respectively.
↑ The first possible notification.
The device may have been connected by this point, in which case the connection prompt would be withheld.
Now, check if the ser
object is defined (i.e., if the serial port was opened):
xxxxxxxxxx
│ try:
│ │ ser
If not, keep trying to open the port assuming that the device will be connected:
xxxxxxxxxx
│ except:
│ │
│ │ while True:
│ │ │
│ │ │ try:
│ │ │ │ ser = Serial(port)
│ │ │ │ break
│ │ │ except:
│ │ │ │ pass
The knee angle data logger has been connected by this point, with or without a prompt to the user.
A notification that the device was connected is to be sent. I assume that an arbitrary notification may already be present, including but by no means limited to the connection prompt from before. If this is the case, it would delay the notification that the device was connected until ‘timing out’ (for lack of a better term). With this kind of notification, the user should know the time since its corresponding event actually occurred.
As such, start a ‘timer’ (i.e., log the current time connected_tick
):
xxxxxxxxxx
connected_tick = time()
Subsequently,
- keep updating the suspected end time
connected_tock
and the elapsed time calculated from it, - keep trying to show a notification that the knee angle data logger was connected and of how long ago this event actually occurred,
and - keep checking if the serial port can be read from (i.e., if the device was not since disconnected),
all until the notification is sent:
xxxxxxxxxx
while True:
│
│ connected_tock = time()
│ connected_time = connected_tock - connected_tick
│
│ if toaster.show_toast('Knee angle data logger connected %.1f seconds ago'
│ │ % connected_time,
│ │ 'Starting now, you may disconnect',
│ │ threaded = True):
│ │ break
│
│ try:
│ │ ser.readline()
│ except:
│ │ try:
│ │ disconnected_tick
│ │ except:
│ │ disconnected_tick = time()
↑ The second possible notification.
The user would be ‘permitted’ to disconnect the device as soon as it is connected if the previous busy waiting while
loop is manually (albeit awkwardly) threaded with the upcoming data logging one, or if done using the threading
standard Python module (documentation) instead. However, the user should know not when the device is simply plugged in (as they do and need not be notified), but when the serial communication link is established soon thereafter.
Now, initialize an empty list of lines
to be read from the serial port, assuming that it is still open:
xxxxxxxxxx
lines = []
Keep trying to read ASCII characters from the serial port, and add them to the list of lines
thereof, until the port is no longer open (i.e., until the device is disconnected):
xxxxxxxxxx
while True:
│
│ try:
│ │ lines.append(ser.readline())
│ except:
│ │ break
Now, check if a variable disconnected_tick
is already defined (i.e., if the device was disconnected while waiting to send the previous notification):
xxxxxxxxxx
try:
│ disconnected_tick
If not, start another ‘timer’ (i.e., log the current time disconnected_tick
):
xxxxxxxxxx
except:
│ disconnected_tick = time()
Keep trying to show a notification that the knee angle data logger was disconnected, and of how long ago this event actually occurred:
xxxxxxxxxx
while True:
│
│ disconnected_tock = time()
│ disconnected_time = disconnected_tock - disconnected_tick
│
│ if toaster.show_toast('Knee angle data logger disconnected %.1f seconds ago' % disconnected_time, ' ', threaded = True):
│ │ break
↑ The third possible notification.
3. Preparing the Collected Knee Angle Data
See the following numbered, broken-down block of code.
Now,
Trim the last two line-ending ASCII characters: line feed <LF>
b'\n'
and carriage return <CR>b'\r'
.Typecast (convert) the remaining ASCII characters from a byte literal to a float.
Do the above for all lines but the first one, which may have been cut off.
This is a list comprehension.From the list, construct a NumPy
ndarray
, , which is a time series of knee angle data.- Reference: numpy.ndarray.
- Reference: The N-dimensional array (ndarray).
xxxxxxxxxx
1. # line[:-2]
2. # float(│ │)
3. # [│ │ for line in lines[1:-1]]
4. y = np.array([float(line[:-2]) for line in lines[1:-1]])
Specify the scalar time interval between knee angle data points and generate a time array,
xDt = 10e-3
t = np.arange(len(y)) * Dt
Specify a moving average window and use it to smooth the knee angle data:
xxxxxxxxxx
window = 5
y_smooth = pd.Series(y).rolling(window, center = True).mean().to_numpy()
4. Making the Results Accessible
Firstly, export the knee angle data to a CSV file for reference:
xxxxxxxxxx
np.savetxt('kneeAngleData.csv', y, fmt = '%.1f')
Secondly, plot the knee angle data points using the Plotly graphing library:
xxxxxxxxxx
data = go.Scatter(x = t, y = y_smooth)
fig = go.Figure(data)
fig.update_layout(xaxis_title = 'Time in Seconds', yaxis_title = 'Knee Angle in Degrees')
fig.update_layout(title = 'Knee Angle Data')
fig.show()
This marks the end of the program.
Appendix
Optional refactor 0:
- Use
try
-except
-*else
* and/ortry
-except
-*finally
* blocks.
Optional refactor 1:
xxxxxxxxxx
1 try:
2 │ ser = Serial(port)
3 except:
4 │ ...
to
xxxxxxxxxx
1 from serial.tools.list_ports import comports
2
3 if port in [comport.device for comport in comports()]:
4 │ ser = Serial(port)
5 elif:
6 │ ...
However, between lines 3 and 4 above, the serial port might become unavailable, in which case Serial
would throw an uncaught SerialException
error.
Optional refactor 2:
xxxxxxxxxx
1 try:
2 │ ser.readline()
3 except:
4 │ ...
to
xxxxxxxxxx
1 from serial.tools.list_ports import comports
2
3 if port not in [comport.device for comport in comports()]:
4 │ ...
Example: A More Complex Application in a Simple Windows UI…
The following fully-assembled Python module/script runs a very simple Windows UI for interfacing with an RC car, all made by me.
from serial import Serial
from win10toast import ToastNotifier
from time import time
from threading import Thread
import ctypes
import tkinter as tk
toaster = ToastNotifier()
port = 'COM4' # 'COM3'
try:
ser = Serial(port, baudrate = 115_200)
except:
while True:
if toaster.show_toast('Connect the BLE link via USB', ' ',
icon_path = 'ico/connect.ico',
threaded = True):
break
try:
ser = Serial(port, baudrate = 115_200)
break
except:
pass
try:
ser
except:
while True:
try:
ser = Serial(port, baudrate = 115_200)
break
except:
pass
connected_notified = False
def connected_notifier():
global connected_notified
connected_tick = time()
while True:
connected_tock = time()
connected_time = connected_tock - connected_tick
if toaster.show_toast('BLE link connected',
'%.1f seconds ago' % connected_time,
icon_path = 'ico/connected.ico',
threaded = True):
connected_notified = True
break
print('connected_notifier waiting')
Thread(target = connected_notifier).start()
ctypes.windll.shcore.SetProcessDpiAwareness(True)
window = tk.Tk()
# window.resizable(False, False)
window.configure(bg = 'white')
window.iconbitmap('ico/window.ico')
window.title('RC Car Interface')
with open('rc_car_interface_instructions.txt') as file:
instructions = ''.join(file.readlines())
label = tk.Label(text = instructions, justify = tk.LEFT, font = ('Segoe UI Semilight', 12))
label.config(bg = 'white')
label.pack(padx = 100, pady = 100)
def disconnected_notifier():
disconnected_tick = time()
while True:
disconnected_tock = time()
if connected_notified:
disconnected_time = disconnected_tock - disconnected_tick
if toaster.show_toast('BLE link disconnected',
'%.1f seconds ago' % disconnected_time,
icon_path = 'ico/disconnected.ico',
threaded = True):
break
print('disconnected_notifier waiting')
disconnected = False
closed = False
def disconnected_checker():
global disconnected
while True:
if not closed:
try:
ser.read()
except:
disconnected = True
try:
window.destroy()
window.quit()
except:
pass
disconnected_notifier()
break
else:
break
print('disconnected_checker waiting')
Thread(target = disconnected_checker).start()
commands = ['v', 'l', 'B', 'r', 'a', 's', 'b', 'L', 'F', 'R']
with open('rc_car_interface_actions.txt') as file:
actions = file.readlines()
def keypress_handler(event):
try:
index = int(event.char)
command = commands[index]
action = actions[index]
ser.write(command.encode())
print(action[:-1])
except:
try:
ser.write(b's')
window.destroy()
window.quit()
except:
pass
print('keypress_handler called')
window.bind('<Key>', keypress_handler)
window.mainloop()
closed = True
if not disconnected:
def closed_notifier():
closed_tick = time()
while True:
closed_tock = time()
if connected_notified:
closed_time = closed_tock - closed_tick
if toaster.show_toast('BLE link interface closed',
'%.1f seconds ago' % closed_time,
icon_path = 'ico/closed.ico',
threaded = True):
break
print('closed_notifier waiting')
Thread(target = closed_notifier).start()
ser.__del__()