ast_monitor 0.5.0__py3-none-any.whl
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.
- ast_monitor/__init__.py +30 -0
- ast_monitor/basic_data.py +116 -0
- ast_monitor/digital_twin.py +155 -0
- ast_monitor/goals_processor.py +106 -0
- ast_monitor/gps_sensor.py +81 -0
- ast_monitor/hr_sensor.py +71 -0
- ast_monitor/html_request_handler.py +24 -0
- ast_monitor/icons/arrow_down.svg +61 -0
- ast_monitor/icons/arrow_left.svg +64 -0
- ast_monitor/icons/arrow_right.svg +64 -0
- ast_monitor/icons/arrow_up.svg +61 -0
- ast_monitor/icons/ascent.svg +64 -0
- ast_monitor/icons/bicycle.svg +65 -0
- ast_monitor/icons/clock.svg +148 -0
- ast_monitor/icons/distance.svg +136 -0
- ast_monitor/icons/heart.svg +79 -0
- ast_monitor/icons/licence.txt +1 -0
- ast_monitor/icons/play.svg +74 -0
- ast_monitor/icons/shutdown.svg +74 -0
- ast_monitor/icons/speed.svg +127 -0
- ast_monitor/icons/stop.svg +63 -0
- ast_monitor/interval_training.py +184 -0
- ast_monitor/mainwindow.py +1308 -0
- ast_monitor/map/README.md +59 -0
- ast_monitor/map/env.d.ts +1 -0
- ast_monitor/map/index.html +16 -0
- ast_monitor/map/package-lock.json +5243 -0
- ast_monitor/map/package.json +37 -0
- ast_monitor/map/public/favicon.ico +0 -0
- ast_monitor/map/src/App.vue +76 -0
- ast_monitor/map/src/assets/base.css +73 -0
- ast_monitor/map/src/assets/logo.svg +1 -0
- ast_monitor/map/src/assets/main.css +1 -0
- ast_monitor/map/src/components/DisplayCard.vue +68 -0
- ast_monitor/map/src/components/Map.vue +78 -0
- ast_monitor/map/src/components/RouteProgress.vue +102 -0
- ast_monitor/map/src/components/TrainingMetrics.vue +45 -0
- ast_monitor/map/src/components/WelcomeItem.vue +87 -0
- ast_monitor/map/src/components/icons/IconCommunity.vue +7 -0
- ast_monitor/map/src/components/icons/IconDocumentation.vue +7 -0
- ast_monitor/map/src/components/icons/IconEcosystem.vue +7 -0
- ast_monitor/map/src/components/icons/IconSupport.vue +7 -0
- ast_monitor/map/src/components/icons/IconTooling.vue +19 -0
- ast_monitor/map/src/main.ts +25 -0
- ast_monitor/map/src/stores/counter.ts +12 -0
- ast_monitor/map/tsconfig.app.json +20 -0
- ast_monitor/map/tsconfig.json +11 -0
- ast_monitor/map/tsconfig.node.json +16 -0
- ast_monitor/map/vite.config.ts +18 -0
- ast_monitor/model.py +521 -0
- ast_monitor/route_reader.py +62 -0
- ast_monitor/simulation.py +117 -0
- ast_monitor/training_session.py +44 -0
- ast_monitor/write_log.py +50 -0
- ast_monitor-0.5.0.dist-info/LICENSE +21 -0
- ast_monitor-0.5.0.dist-info/METADATA +293 -0
- ast_monitor-0.5.0.dist-info/RECORD +58 -0
- ast_monitor-0.5.0.dist-info/WHEEL +4 -0
ast_monitor/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from ast_monitor.basic_data import BasicData
|
|
2
|
+
from ast_monitor.digital_twin import DigitalTwin
|
|
3
|
+
from ast_monitor.goals_processor import GoalsProcessor
|
|
4
|
+
from ast_monitor.gps_sensor import GpsSensor
|
|
5
|
+
from ast_monitor.hr_sensor import HrSensor
|
|
6
|
+
from ast_monitor.interval_training import IntervalTraining
|
|
7
|
+
from ast_monitor.mainwindow import Ui_MainWindow
|
|
8
|
+
from ast_monitor.model import AST
|
|
9
|
+
from ast_monitor.route_reader import RouteReader
|
|
10
|
+
from ast_monitor.simulation import Simulation
|
|
11
|
+
from ast_monitor.training_session import TrainingSession
|
|
12
|
+
from ast_monitor.write_log import WriteLog
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'BasicData',
|
|
16
|
+
'DigitalTwin',
|
|
17
|
+
'GpsSensor',
|
|
18
|
+
'HrSensor',
|
|
19
|
+
'IntervalTraining',
|
|
20
|
+
'Ui_MainWindow',
|
|
21
|
+
'AST',
|
|
22
|
+
'GoalsProcessor',
|
|
23
|
+
'RouteReader',
|
|
24
|
+
'Simulation',
|
|
25
|
+
'TrainingSession',
|
|
26
|
+
'WriteLog'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
__project__ = 'ast_monitor'
|
|
30
|
+
__version__ = '0.5.0'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BasicData:
|
|
7
|
+
"""
|
|
8
|
+
Class for storing and tracking the basic training data in real time.\n
|
|
9
|
+
Args:
|
|
10
|
+
hr_data_path (str):
|
|
11
|
+
path to the file that contains HR data
|
|
12
|
+
gps_data_path (str):
|
|
13
|
+
path to the file that contains GPS data
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, hr_data_path: str, gps_data_path: str) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initialization method for BasicData class.\n
|
|
18
|
+
Args:
|
|
19
|
+
hr_data_path (str):
|
|
20
|
+
path to the file that contains HR data
|
|
21
|
+
gps_data_path (str):
|
|
22
|
+
path to the file that contains GPS data
|
|
23
|
+
"""
|
|
24
|
+
self.hr_data_path = hr_data_path
|
|
25
|
+
self.gps_data_path = gps_data_path
|
|
26
|
+
self.current_speed = None
|
|
27
|
+
self.current_heart_rate = None
|
|
28
|
+
self.previous_gps = None
|
|
29
|
+
self.current_gps = None
|
|
30
|
+
self.distance = 0.0
|
|
31
|
+
|
|
32
|
+
with open(self.gps_data_path, 'a') as f:
|
|
33
|
+
f.truncate(0)
|
|
34
|
+
with open(self.hr_data_path, 'a') as f:
|
|
35
|
+
f.truncate(0)
|
|
36
|
+
|
|
37
|
+
def read_current_hr(self) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Reading the current HR from the file.
|
|
40
|
+
"""
|
|
41
|
+
if not os.path.exists(self.hr_data_path):
|
|
42
|
+
self.current_heart_rate = None
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
with open(self.hr_data_path, 'rb') as f:
|
|
46
|
+
try:
|
|
47
|
+
f.seek(-2, os.SEEK_END)
|
|
48
|
+
while f.read(1) != b'\n':
|
|
49
|
+
f.seek(-2, os.SEEK_CUR)
|
|
50
|
+
except OSError:
|
|
51
|
+
f.seek(0)
|
|
52
|
+
try:
|
|
53
|
+
hr = int(f.readline().decode().rstrip())
|
|
54
|
+
self.current_heart_rate = str(hr)
|
|
55
|
+
except ValueError:
|
|
56
|
+
self.current_heart_rate = None
|
|
57
|
+
|
|
58
|
+
def read_current_gps(self) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Reading the current GPS position from the file.
|
|
61
|
+
"""
|
|
62
|
+
if not os.path.exists(self.gps_data_path):
|
|
63
|
+
self.current_speed = None
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
with open(self.gps_data_path, 'rb') as f:
|
|
67
|
+
try:
|
|
68
|
+
f.seek(-2, os.SEEK_END)
|
|
69
|
+
while f.read(1) != b'\n':
|
|
70
|
+
f.seek(-2, os.SEEK_CUR)
|
|
71
|
+
except OSError:
|
|
72
|
+
f.seek(0)
|
|
73
|
+
try:
|
|
74
|
+
self.previous_gps = self.current_gps
|
|
75
|
+
|
|
76
|
+
# Reading the last GPS data.
|
|
77
|
+
gps = f.readline().decode().rstrip().split(';')
|
|
78
|
+
self.current_gps = (
|
|
79
|
+
float(gps[1]), float(gps[0]), float(gps[2]), float(gps[3])
|
|
80
|
+
)
|
|
81
|
+
except IndexError:
|
|
82
|
+
self.previous_gps = None
|
|
83
|
+
self.current_gps = None
|
|
84
|
+
|
|
85
|
+
def calculate_speed(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Calculating the speed between the previous and the current trackpoint.
|
|
88
|
+
"""
|
|
89
|
+
if not self.previous_gps or not self.current_gps:
|
|
90
|
+
self.current_speed = None
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# If the last measure is older than 1.5 seconds,
|
|
94
|
+
# it is considered deprecated.
|
|
95
|
+
if time.time() - self.current_gps[3] > 1.5:
|
|
96
|
+
self.current_speed = None
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Calculating the distance.
|
|
100
|
+
R = 6373.0
|
|
101
|
+
lat1 = math.radians(self.previous_gps[0])
|
|
102
|
+
lat2 = math.radians(self.current_gps[0])
|
|
103
|
+
lon1 = math.radians(self.previous_gps[1])
|
|
104
|
+
lon2 = math.radians(self.current_gps[1])
|
|
105
|
+
dlon = lon2 - lon1
|
|
106
|
+
dlat = lat2 - lat1
|
|
107
|
+
a = (
|
|
108
|
+
math.sin(dlat / 2)**2 +
|
|
109
|
+
math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
|
|
110
|
+
)
|
|
111
|
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
112
|
+
|
|
113
|
+
self.distance = 1000 * R * c
|
|
114
|
+
t = self.current_gps[3] - self.previous_gps[3]
|
|
115
|
+
if t:
|
|
116
|
+
self.current_speed = 3.6 * self.distance / t
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DigitalTwin:
|
|
5
|
+
"""
|
|
6
|
+
Class that represents a digital twin that
|
|
7
|
+
uses a mathematical prediction model to
|
|
8
|
+
analyze an exercise in the real time.\n
|
|
9
|
+
Args:
|
|
10
|
+
predicted_heart_rate (int):
|
|
11
|
+
a predicted heart rate of an interval
|
|
12
|
+
(speed or rest segment) in beats per minute
|
|
13
|
+
duration (int):
|
|
14
|
+
duration of an interval in minutes
|
|
15
|
+
"""
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
proposed_heart_rate: int,
|
|
19
|
+
duration: int,
|
|
20
|
+
tick_time: float,
|
|
21
|
+
basic_data
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialization method of the DigitalTwin class.\n
|
|
25
|
+
Args:
|
|
26
|
+
predicted_heart_rate (int):
|
|
27
|
+
a predicted heart rate of an interval
|
|
28
|
+
(speed or rest segment) in beats per minute
|
|
29
|
+
duration (int):
|
|
30
|
+
duration of an interval in minutes
|
|
31
|
+
tick_time (float):
|
|
32
|
+
a tick time in seconds
|
|
33
|
+
"""
|
|
34
|
+
self.proposed_heart_rate = proposed_heart_rate
|
|
35
|
+
self.predicted_duration = 60 * duration
|
|
36
|
+
self.current_heart_rate = 0
|
|
37
|
+
self.start_time = time.time()
|
|
38
|
+
self.expected_trackpoints = (60 * 1000 * duration) // tick_time
|
|
39
|
+
self.basic_data = basic_data
|
|
40
|
+
|
|
41
|
+
def read_control_data(self) -> tuple:
|
|
42
|
+
"""
|
|
43
|
+
Reading the current heart rate and the interval duration.\n
|
|
44
|
+
Returns:
|
|
45
|
+
tuple[int, float]: current heart rate, current duration
|
|
46
|
+
"""
|
|
47
|
+
# Reading the current heart rate.
|
|
48
|
+
hr = self.basic_data.current_heart_rate
|
|
49
|
+
if hr:
|
|
50
|
+
hr = int(hr)
|
|
51
|
+
|
|
52
|
+
# Calculating the current phase time.
|
|
53
|
+
phase_time = time.time() - self.start_time
|
|
54
|
+
|
|
55
|
+
return hr, phase_time
|
|
56
|
+
|
|
57
|
+
def calculate_prediction(
|
|
58
|
+
self,
|
|
59
|
+
trimp_delta: float,
|
|
60
|
+
average_heart_rate: float,
|
|
61
|
+
proposed_heart_rate: int,
|
|
62
|
+
time_delta: float,
|
|
63
|
+
expected_trackpoints: int
|
|
64
|
+
) -> float:
|
|
65
|
+
"""
|
|
66
|
+
Calculating the predicted heart rate.\n
|
|
67
|
+
Args:
|
|
68
|
+
trimp_delta (float):
|
|
69
|
+
the calculated TRIMP difference
|
|
70
|
+
average_heart_rate (float):
|
|
71
|
+
the average heart rate of the interval in beats per minute
|
|
72
|
+
proposed_heart_rate (int):
|
|
73
|
+
the proposed heart rate of the interval in beats per minute
|
|
74
|
+
time_delta (float):
|
|
75
|
+
the time difference in seconds
|
|
76
|
+
expected_trackpoints (int):
|
|
77
|
+
the number of expected trackpoints
|
|
78
|
+
Returns:
|
|
79
|
+
float: the proposed heart rate
|
|
80
|
+
"""
|
|
81
|
+
if trimp_delta < 0:
|
|
82
|
+
self.minus += trimp_delta
|
|
83
|
+
else:
|
|
84
|
+
self.plus += trimp_delta
|
|
85
|
+
|
|
86
|
+
if average_heart_rate < proposed_heart_rate:
|
|
87
|
+
return proposed_heart_rate
|
|
88
|
+
else:
|
|
89
|
+
return (
|
|
90
|
+
proposed_heart_rate +
|
|
91
|
+
(self.minus - self.plus) / (expected_trackpoints * time_delta)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def predict_heart_rate(self) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Digital twin algorithm that monitors the athlete
|
|
97
|
+
performance in the real time and calculates the
|
|
98
|
+
predicted heart rate.
|
|
99
|
+
"""
|
|
100
|
+
n = 1
|
|
101
|
+
current_time = 0
|
|
102
|
+
current_heart_rate = 0
|
|
103
|
+
previous_heart_rate, previous_time = self.read_control_data()
|
|
104
|
+
self.average_heart_rate = previous_heart_rate
|
|
105
|
+
time_delta = 0
|
|
106
|
+
self.minus, self.plus = 0, 0
|
|
107
|
+
|
|
108
|
+
while current_time <= self.predicted_duration - 1:
|
|
109
|
+
# Reading the current heart rate and the duration.
|
|
110
|
+
current_heart_rate, current_time = self.read_control_data()
|
|
111
|
+
|
|
112
|
+
time_delta = current_time - previous_time
|
|
113
|
+
if time_delta == 0:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if self.predicted_duration - 1 <= current_time:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
if current_heart_rate and previous_heart_rate:
|
|
120
|
+
# Calculating the average heart rate and the time difference.
|
|
121
|
+
if self.average_heart_rate:
|
|
122
|
+
self.average_heart_rate = (
|
|
123
|
+
self.average_heart_rate +
|
|
124
|
+
(1 / n) * (
|
|
125
|
+
current_heart_rate - self.average_heart_rate
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
self.average_heart_rate = current_heart_rate
|
|
130
|
+
|
|
131
|
+
# Calculating the TRIMP difference.
|
|
132
|
+
trimp_delta = (
|
|
133
|
+
((current_heart_rate - previous_heart_rate) / 2) *
|
|
134
|
+
time_delta -
|
|
135
|
+
self.proposed_heart_rate * time_delta
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Predicted heart rate calculation.
|
|
139
|
+
self.predicted_heart_rate = self.calculate_prediction(
|
|
140
|
+
trimp_delta,
|
|
141
|
+
self.average_heart_rate,
|
|
142
|
+
self.proposed_heart_rate,
|
|
143
|
+
self.predicted_duration - current_time,
|
|
144
|
+
self.expected_trackpoints
|
|
145
|
+
)
|
|
146
|
+
if self.predicted_heart_rate:
|
|
147
|
+
self.predicted_heart_rate = round(
|
|
148
|
+
self.predicted_heart_rate
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
n += 1
|
|
152
|
+
|
|
153
|
+
previous_time = current_time
|
|
154
|
+
previous_heart_rate = current_heart_rate
|
|
155
|
+
time.sleep(0.5)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GoalsProcessor:
|
|
5
|
+
def __init__(self, route):
|
|
6
|
+
"""Class for measuring the progress of the user on the route."""
|
|
7
|
+
self.position = None
|
|
8
|
+
self.total_ascent = route["evaluation"]["total_ascent"]
|
|
9
|
+
self.total_distance = route["evaluation"]["total_distance"]
|
|
10
|
+
self.ascents = route["ascents"]
|
|
11
|
+
self.distances = route["distances"]
|
|
12
|
+
self.current_distance = 0
|
|
13
|
+
self.current_ascent = 0
|
|
14
|
+
self.progress = 0
|
|
15
|
+
self.distance_to_go = sum(self.distances)
|
|
16
|
+
self.ascent_to_go = sum(self.ascents)
|
|
17
|
+
self.index_closest = None
|
|
18
|
+
self.route = route
|
|
19
|
+
filteredIntersections = [sublist[1] for sublist in route["nodes"]]
|
|
20
|
+
|
|
21
|
+
# Filter the list of dictionaries based on the IDs and remove duplicates
|
|
22
|
+
filtered_list = []
|
|
23
|
+
last_added_id = None
|
|
24
|
+
|
|
25
|
+
for obj in route['route_render']:
|
|
26
|
+
# Check if the id is in filteredIntersections
|
|
27
|
+
if obj['id'] in filteredIntersections:
|
|
28
|
+
# Check if the id is the same as the last added id
|
|
29
|
+
if obj['id'] != last_added_id:
|
|
30
|
+
# Add the object to the filtered list
|
|
31
|
+
filtered_list.append(obj)
|
|
32
|
+
|
|
33
|
+
# Update the last added id
|
|
34
|
+
last_added_id = obj['id']
|
|
35
|
+
|
|
36
|
+
# Now filtered_list contains the filtered and unique dictionaries
|
|
37
|
+
self.filtered_list = filtered_list
|
|
38
|
+
a = 100
|
|
39
|
+
|
|
40
|
+
def add_position(self, position):
|
|
41
|
+
"""Add the current position of the user."""
|
|
42
|
+
self.position = position[0:2]
|
|
43
|
+
self.get_progress(self.position)
|
|
44
|
+
|
|
45
|
+
def get_progress(self, position):
|
|
46
|
+
"""Calculate the progress of the user on the route, (%) of route, Ascent, Distance."""
|
|
47
|
+
current_node = self.find_closest_object(position)
|
|
48
|
+
distance = self.haversine_distance(position, (current_node['lat'], current_node['lon']))
|
|
49
|
+
if distance < 0.05:
|
|
50
|
+
self.current_distance += sum(self.distances[0:self.index_closest + 1])
|
|
51
|
+
self.current_ascent += sum(self.ascents[0:self.index_closest + 1])
|
|
52
|
+
# remove the summed indexes
|
|
53
|
+
self.distances = self.distances[self.index_closest + 1:]
|
|
54
|
+
self.ascents = self.ascents[self.index_closest + 1:]
|
|
55
|
+
self.filtered_list = self.filtered_list[self.index_closest + 1:]
|
|
56
|
+
self.index_closest = None
|
|
57
|
+
self.distance_to_go = self.total_distance - self.current_distance
|
|
58
|
+
self.ascent_to_go = self.total_ascent - self.current_ascent
|
|
59
|
+
self.progress = self.current_distance / self.total_distance
|
|
60
|
+
|
|
61
|
+
def find_closest_object(self, position):
|
|
62
|
+
"""Finds the closest object in the list to the given point."""
|
|
63
|
+
smallest_diff = float('inf')
|
|
64
|
+
closest_object = None
|
|
65
|
+
|
|
66
|
+
index = 0
|
|
67
|
+
# Loop through each object in the list
|
|
68
|
+
for obj in self.filtered_list:
|
|
69
|
+
lat_diff = math.fabs(obj['lat'] - position[0])
|
|
70
|
+
lon_diff = math.fabs(obj['lon'] - position[1])
|
|
71
|
+
|
|
72
|
+
# Calculate the total difference for both latitude and longitude
|
|
73
|
+
total_diff = lat_diff + lon_diff
|
|
74
|
+
|
|
75
|
+
# Update smallest_diff and closest_object if this object is closer to the given point
|
|
76
|
+
if total_diff < smallest_diff:
|
|
77
|
+
smallest_diff = total_diff
|
|
78
|
+
closest_object = obj
|
|
79
|
+
self.index_closest = index
|
|
80
|
+
index += 1
|
|
81
|
+
|
|
82
|
+
return closest_object # Return the closest object
|
|
83
|
+
|
|
84
|
+
def haversine_distance(self, coord1, coord2):
|
|
85
|
+
"""
|
|
86
|
+
Calculate the Haversine distance between two points on the Earth specified by latitude/longitude.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
# Radius of Earth in kilometers
|
|
90
|
+
R = 6371.0
|
|
91
|
+
|
|
92
|
+
# Convert latitude and longitude from degrees to radians
|
|
93
|
+
lat1, lon1 = math.radians(coord1[0]), math.radians(coord1[1])
|
|
94
|
+
lat2, lon2 = math.radians(coord2[0]), math.radians(coord2[1])
|
|
95
|
+
|
|
96
|
+
# Compute differences in latitude and longitude
|
|
97
|
+
dlat = lat2 - lat1
|
|
98
|
+
dlon = lon2 - lon1
|
|
99
|
+
|
|
100
|
+
# Haversine formula
|
|
101
|
+
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
102
|
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
103
|
+
|
|
104
|
+
distance = R * c
|
|
105
|
+
|
|
106
|
+
return distance
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
# It should work on ARM architectures only.
|
|
4
|
+
try:
|
|
5
|
+
import adafruit_gps
|
|
6
|
+
import serial
|
|
7
|
+
except BaseException:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GpsSensor:
|
|
12
|
+
"""
|
|
13
|
+
Class for working with GPS sensor.\n
|
|
14
|
+
Args:
|
|
15
|
+
gps_path (str):
|
|
16
|
+
path to file for storing GPS data
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, gps_path: str = 'sensor_data/gps.txt') -> None:
|
|
19
|
+
"""
|
|
20
|
+
Initialisation method for GpsSensor class.\n
|
|
21
|
+
Args:
|
|
22
|
+
gps_path (str):
|
|
23
|
+
path to file for storing gps data
|
|
24
|
+
"""
|
|
25
|
+
self.gps_path = gps_path
|
|
26
|
+
|
|
27
|
+
def write_gps_data_to_file(
|
|
28
|
+
self,
|
|
29
|
+
longitude: float,
|
|
30
|
+
latitude: float,
|
|
31
|
+
altitude: float
|
|
32
|
+
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Method for writing GPS data to text file.\n
|
|
35
|
+
Args:
|
|
36
|
+
longitude (float):
|
|
37
|
+
longitude on Earth
|
|
38
|
+
latitude (float):
|
|
39
|
+
latitude on Earth
|
|
40
|
+
altitude (float):
|
|
41
|
+
current altitude
|
|
42
|
+
"""
|
|
43
|
+
output = (
|
|
44
|
+
str(longitude) + ';' + str(latitude) + ';' +
|
|
45
|
+
str(altitude) + ';' + time.time()
|
|
46
|
+
)
|
|
47
|
+
with open(self.gps_path, 'a') as f:
|
|
48
|
+
f.write(output + '\n')
|
|
49
|
+
|
|
50
|
+
def get_gps_data(self) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Method for listening the channel for obtaining GPS data from sensor.\n
|
|
53
|
+
Note: Example is based on source code from
|
|
54
|
+
https://github.com/adafruit/Adafruit_CircuitPython_GPS
|
|
55
|
+
"""
|
|
56
|
+
uart = serial.Serial('/dev/serial0', baudrate=9600, timeout=10)
|
|
57
|
+
gps = adafruit_gps.GPS(uart, debug=False)
|
|
58
|
+
gps.send_command(b'PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0')
|
|
59
|
+
gps.send_command(b'PMTK220,1000')
|
|
60
|
+
last_print = time.monotonic()
|
|
61
|
+
|
|
62
|
+
while True:
|
|
63
|
+
gps.update()
|
|
64
|
+
|
|
65
|
+
current = time.monotonic()
|
|
66
|
+
if current - last_print >= 1.0:
|
|
67
|
+
last_print = current
|
|
68
|
+
if not gps.has_fix:
|
|
69
|
+
print('Waiting for fix...')
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
LATITUDE = gps.latitude
|
|
73
|
+
LONGITUDE = gps.longitude
|
|
74
|
+
ALTITUDE = gps.altitude_m
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
(LATITUDE is not None) and
|
|
78
|
+
(LONGITUDE is not None) and
|
|
79
|
+
(ALTITUDE is not None)
|
|
80
|
+
):
|
|
81
|
+
self.write_gps_data_to_file(LONGITUDE, LATITUDE, ALTITUDE)
|
ast_monitor/hr_sensor.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# If openant dependency is missing.
|
|
2
|
+
try:
|
|
3
|
+
from ant.easy.node import Node
|
|
4
|
+
from ant.easy.channel import Channel
|
|
5
|
+
except BaseException:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HrSensor():
|
|
10
|
+
"""
|
|
11
|
+
Class for working with HR sensor.\n
|
|
12
|
+
Args:
|
|
13
|
+
hr_path (str):
|
|
14
|
+
path to file for storing HR data
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, hr_path='sensor_data/hr.txt') -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialisation method for HrSensor class.\n
|
|
19
|
+
Args:
|
|
20
|
+
hr_path (str):
|
|
21
|
+
path to file for storing HR data
|
|
22
|
+
"""
|
|
23
|
+
self.hr_path = hr_path
|
|
24
|
+
|
|
25
|
+
def write_hr_data_to_file(self, hr: int) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Method for writing hr data to text file.\n
|
|
28
|
+
Args:
|
|
29
|
+
hr (int):
|
|
30
|
+
heart rate
|
|
31
|
+
"""
|
|
32
|
+
with open(self.hr_path, 'a') as f:
|
|
33
|
+
f.write(hr + "\n")
|
|
34
|
+
|
|
35
|
+
def on_data(self, data) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Extracting and writing heart rate data to file.\n
|
|
38
|
+
Args:
|
|
39
|
+
data ():
|
|
40
|
+
list that contains heart rate
|
|
41
|
+
"""
|
|
42
|
+
heartrate = data[7]
|
|
43
|
+
print(heartrate)
|
|
44
|
+
self.write_hr_data_to_file(str(heartrate))
|
|
45
|
+
|
|
46
|
+
def get_hr_data(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Method for listening the channel for obtaining HR data from sensor.\n
|
|
49
|
+
Note: Example is based on source code from
|
|
50
|
+
https://github.com/Tigge/openant/blob/master/examples/heart_rate_monitor.py
|
|
51
|
+
"""
|
|
52
|
+
NETWORK_KEY = [0xB9, 0xA5, 0x21, 0xFB, 0xBD, 0x72, 0xC3, 0x45]
|
|
53
|
+
|
|
54
|
+
node = Node()
|
|
55
|
+
node.set_network_key(0x00, NETWORK_KEY)
|
|
56
|
+
|
|
57
|
+
channel = node.new_channel(Channel.Type.BIDIRECTIONAL_RECEIVE)
|
|
58
|
+
|
|
59
|
+
channel.on_broadcast_data = self.on_data
|
|
60
|
+
channel.on_burst_data = self.on_data
|
|
61
|
+
|
|
62
|
+
channel.set_period(8070)
|
|
63
|
+
channel.set_search_timeout(12)
|
|
64
|
+
channel.set_rf_freq(57)
|
|
65
|
+
channel.set_id(0, 120, 0)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
channel.open()
|
|
69
|
+
node.start()
|
|
70
|
+
finally:
|
|
71
|
+
node.stop()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import http.server
|
|
2
|
+
import os
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CustomHandler(http.server.SimpleHTTPRequestHandler):
|
|
7
|
+
"""Custom handler to serve the html files from the dist folder instead of the root folder.
|
|
8
|
+
The handler helps us avoid changing the current directory of the Python script."""
|
|
9
|
+
|
|
10
|
+
def translate_path(self, path):
|
|
11
|
+
"""
|
|
12
|
+
Method that allows the server to serve the html files from the dist folder instead of the root folder.
|
|
13
|
+
Args:
|
|
14
|
+
path: url path
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
parsed_path = urlparse(path).path
|
|
20
|
+
current_file_directory = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
custom_folder = os.path.join(current_file_directory, 'map', 'dist')
|
|
22
|
+
translated_path = os.path.join(custom_folder, parsed_path.lstrip('/'))
|
|
23
|
+
print(f"Translated path: {translated_path}") # Debugging line to print the translated path
|
|
24
|
+
return translated_path
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<svg
|
|
3
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
4
|
+
xmlns:cc="http://creativecommons.org/ns#"
|
|
5
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
6
|
+
xmlns:svg="http://www.w3.org/2000/svg"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
9
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
10
|
+
width="6.5972247mm"
|
|
11
|
+
height="8.3541622mm"
|
|
12
|
+
viewBox="0 0 6.5972248 8.354162"
|
|
13
|
+
version="1.1"
|
|
14
|
+
id="svg1844"
|
|
15
|
+
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
|
16
|
+
sodipodi:docname="arrow_down.svg">
|
|
17
|
+
<defs
|
|
18
|
+
id="defs1838" />
|
|
19
|
+
<sodipodi:namedview
|
|
20
|
+
id="base"
|
|
21
|
+
pagecolor="#ffffff"
|
|
22
|
+
bordercolor="#666666"
|
|
23
|
+
borderopacity="1.0"
|
|
24
|
+
inkscape:pageopacity="0.0"
|
|
25
|
+
inkscape:pageshadow="2"
|
|
26
|
+
inkscape:zoom="11.2"
|
|
27
|
+
inkscape:cx="-1.0870781"
|
|
28
|
+
inkscape:cy="16.256367"
|
|
29
|
+
inkscape:document-units="mm"
|
|
30
|
+
inkscape:current-layer="layer1"
|
|
31
|
+
inkscape:document-rotation="0"
|
|
32
|
+
showgrid="false"
|
|
33
|
+
inkscape:window-width="1920"
|
|
34
|
+
inkscape:window-height="1017"
|
|
35
|
+
inkscape:window-x="-8"
|
|
36
|
+
inkscape:window-y="-8"
|
|
37
|
+
inkscape:window-maximized="1" />
|
|
38
|
+
<metadata
|
|
39
|
+
id="metadata1841">
|
|
40
|
+
<rdf:RDF>
|
|
41
|
+
<cc:Work
|
|
42
|
+
rdf:about="">
|
|
43
|
+
<dc:format>image/svg+xml</dc:format>
|
|
44
|
+
<dc:type
|
|
45
|
+
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
46
|
+
<dc:title></dc:title>
|
|
47
|
+
</cc:Work>
|
|
48
|
+
</rdf:RDF>
|
|
49
|
+
</metadata>
|
|
50
|
+
<g
|
|
51
|
+
inkscape:label="Layer 1"
|
|
52
|
+
inkscape:groupmode="layer"
|
|
53
|
+
id="layer1"
|
|
54
|
+
transform="translate(-109.33837,-90.883931)">
|
|
55
|
+
<path
|
|
56
|
+
style="fill:#5bad5e;fill-opacity:1;stroke:#000000;stroke-width:0.19538px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
57
|
+
d="m 114.5562,91.037913 c 10e-4,0.05431 0,4.250421 0,4.250421 0,0 0.85295,0.01977 1.27904,0 0.11267,-0.0052 -3.07792,3.854675 -3.19759,3.852091 -0.11723,-0.0025 -3.33164,-3.850198 -3.19759,-3.852091 0.42632,-0.0059 1.27903,0 1.27903,0 0,0 2.7e-4,-4.166126 0,-4.250421 -2.5e-4,-0.07785 3.83576,-0.07221 3.83711,0 z"
|
|
58
|
+
id="path1484-2"
|
|
59
|
+
sodipodi:nodetypes="scssscss" />
|
|
60
|
+
</g>
|
|
61
|
+
</svg>
|