diff --git a/modules/config.py b/modules/config.py index 34d22c0e..57d5d828 100644 --- a/modules/config.py +++ b/modules/config.py @@ -5,8 +5,6 @@ import logging import os import shutil -import traceback -import math from glob import glob import numpy as np @@ -14,15 +12,6 @@ from PIL import Image -_IS_RASPI = False -try: - import RPi.GPIO as GPIO - - GPIO.setmode(GPIO.BCM) - _IS_RASPI = True -except ImportError: - pass - from logger import CustomRotatingFileHandler, app_logger from modules.helper.setting import Setting from modules.button_config import Button_Config @@ -31,10 +20,19 @@ exec_cmd_return_value, is_running_as_service, ) +from modules.utils.map import get_maptile_filename, get_tilexy_and_xy_in_tile from modules.utils.timer import Timer BOOT_FILE = "/boot/config.txt" +_IS_RASPI = False +try: + import RPi.GPIO as GPIO + + GPIO.setmode(GPIO.BCM) + _IS_RASPI = True +except ImportError: + pass class Config: @@ -638,33 +636,13 @@ class Config: G_BT_ADDRESSES = {} G_BT_USE_ADDRESS = "" - # for track - TRACK_STR = [ - "N", - "NE", - "E", - "SE", - "S", - "SW", - "W", - "NW", - "N", - ] - - # for get_dist_on_earth - GEO_R1 = 6378.137 - GEO_R2 = 6356.752314140 - GEO_R1_2 = (GEO_R1 * 1000) ** 2 - GEO_R2_2 = (GEO_R2 * 1000) ** 2 - GEO_E2 = (GEO_R1_2 - GEO_R2_2) / GEO_R1_2 - G_DISTANCE_BY_LAT1S = GEO_R2 * 1000 * 2 * np.pi / 360 / 60 / 60 # [m] - ####################### # class objects # ####################### logger = None display = None network = None + api = None bt_pan = None ble_uart = None setting = None @@ -694,9 +672,8 @@ def __init__(self): self.G_FULLSCREEN = True if args.demo: self.G_DUMMY_OUTPUT = True - if args.layout: - if os.path.exists(args.layout): - self.G_LAYOUT_FILE = args.layout + if args.layout and os.path.exists(args.layout): + self.G_LAYOUT_FILE = args.layout if args.headless: self.G_HEADLESS = True # show options @@ -728,6 +705,7 @@ def __init__(self): # map list if os.path.exists(self.G_MAP_LIST): self.read_map_list() + # set default values for map_config in [ self.G_MAP_CONFIG, @@ -802,8 +780,10 @@ async def delay_init(self): # network await self.gui.set_boot_status("initialize network modules...") + from modules.helper.api import api from modules.helper.network import Network + self.api = api(self) self.network = Network(self) # bluetooth @@ -870,38 +850,41 @@ async def delay_init(self): await self.logger.resume_start_stop() async def keyboard_check(self): - while not self.G_QUIT: - app_logger.info( - "s:start/stop, l: lap, r:reset, p: previous screen, n: next screen, q: quit" - ) - key = await self.loop.run_in_executor(None, input, "> ") - - if key == "s": - self.logger.start_and_stop_manual() - elif key == "l": - self.logger.count_laps() - elif key == "r": - self.logger.reset_count() - elif key == "n" and self.gui: - self.gui.scroll_next() - elif key == "p" and self.gui: - self.gui.scroll_prev() - elif key == "q" and self.gui: - await self.quit() - ##### temporary ##### - # test hardware key signals - elif key == "m" and self.gui: - self.gui.enter_menu() - elif key == "v" and self.gui: - self.gui.press_space() - elif key == "," and self.gui: - self.gui.press_tab() - elif key == "." and self.gui: - self.gui.press_shift_tab() - elif key == "b" and self.gui: - self.gui.back_menu() - elif key == "c" and self.gui: - self.gui.get_screenshot() + try: + while True: + app_logger.info( + "s:start/stop, l: lap, r:reset, p: previous screen, n: next screen, q: quit" + ) + key = await self.loop.run_in_executor(None, input, "> ") + + if key == "s": + self.logger.start_and_stop_manual() + elif key == "l": + self.logger.count_laps() + elif key == "r": + self.logger.reset_count() + elif key == "n" and self.gui: + self.gui.scroll_next() + elif key == "p" and self.gui: + self.gui.scroll_prev() + elif key == "q" and self.gui: + await self.quit() + ##### temporary ##### + # test hardware key signals + elif key == "m" and self.gui: + self.gui.enter_menu() + elif key == "v" and self.gui: + self.gui.press_space() + elif key == "," and self.gui: + self.gui.press_tab() + elif key == "." and self.gui: + self.gui.press_shift_tab() + elif key == "b" and self.gui: + self.gui.back_menu() + elif key == "c" and self.gui: + self.gui.get_screenshot() + except asyncio.CancelledError: + pass def set_logger(self, logger): self.logger = logger @@ -919,15 +902,6 @@ def check_map_dir(self): if self.G_LOG_ALTITUDE_FROM_DATA_SOURCE: os.makedirs(os.path.join("maptile", self.G_DEM_MAP), exist_ok=True) - @staticmethod - def remove_maptiles(map_name): - path = os.path.join("maptile", map_name) - if os.path.exists(path): - files = os.listdir(path) - dirs = [f for f in files if os.path.isdir(os.path.join(path, f))] - for d in dirs: - shutil.rmtree(os.path.join(path, d)) - def get_serial(self): if not self.G_IS_RASPI: return @@ -996,9 +970,6 @@ async def quit(self): self.logger.remove_handler() app_logger.info("quit done") - if self.G_GUI_MODE != "PyQt": - self.loop.close() - def poweroff(self): # TODO # should be replaced by quit() with power_off option @@ -1040,7 +1011,7 @@ def hardware_wifi_bt(self, status): "sudo", "sed", "-i", - f"s/^dtoverlay\=disable\-{dev}/\#dtoverlay\=disable\-{dev}/", + rf"s/^dtoverlay\=disable\-{dev}/\#dtoverlay\=disable\-{dev}/", BOOT_FILE, ], False, @@ -1054,7 +1025,7 @@ def hardware_wifi_bt(self, status): "sudo", "sed", "-i", - f"s/^\#dtoverlay\=disable\-{dev}/dtoverlay\=disable\-{dev}/", + rf"s/^\#dtoverlay\=disable\-{dev}/dtoverlay\=disable\-{dev}/", BOOT_FILE, ], False, @@ -1075,7 +1046,7 @@ def hardware_wifi_bt(self, status): "sed", "-i", "-e", - 's/^\#DEVICES\="\/dev\/ttyS0"/DEVICES\="\/dev\/ttyS0"/', + r's/^\#DEVICES\="\/dev\/ttyS0"/DEVICES\="\/dev\/ttyS0"/', "/etc/default/gpsd", ], False, @@ -1086,7 +1057,7 @@ def hardware_wifi_bt(self, status): "sed", "-i", "-e", - 's/^DEVICES\="\/dev\/ttyAMA0"/\#DEVICES\="\/dev\/ttyAMA0"/', + r's/^DEVICES\="\/dev\/ttyAMA0"/\#DEVICES\="\/dev\/ttyAMA0"/', "/etc/default/gpsd", ], False, @@ -1195,94 +1166,12 @@ def read_map_list(self): map_list[key]["attribution"] = "" self.G_MAP_CONFIG.update(map_list) - def get_track_str(self, drc): - track_int = int((drc + 22.5) / 45.0) - return self.TRACK_STR[track_int] - - # return [m] - def get_dist_on_earth(self, p0_lon, p0_lat, p1_lon, p1_lat): - if p0_lon == p1_lon and p0_lat == p1_lat: - return 0 - (r0_lon, r0_lat, r1_lon, r1_lat) = map( - math.radians, [p0_lon, p0_lat, p1_lon, p1_lat] - ) - delta_x = r1_lon - r0_lon - cos_d = math.sin(r0_lat) * math.sin(r1_lat) + math.cos(r0_lat) * math.cos( - r1_lat - ) * math.cos(delta_x) - try: - res = 1000 * math.acos(cos_d) * self.GEO_R1 - return res - except: - # traceback.print_exc() - # print("cos_d =", cos_d) - # print("parameter:", p0_lon, p0_lat, p1_lon, p1_lat) - return 0 - - # return [m] - def get_dist_on_earth_array(self, p0_lon, p0_lat, p1_lon, p1_lat): - # if p0_lon == p1_lon and p0_lat == p1_lat: - # return 0 - r0_lon = np.radians(p0_lon) - r0_lat = np.radians(p0_lat) - r1_lon = np.radians(p1_lon) - r1_lat = np.radians(p1_lat) - # (r0_lon, r0_lat, r1_lon, r1_lat) = map(radians, [p0_lon, p0_lat, p1_lon, p1_lat]) - delta_x = r1_lon - r0_lon - cos_d = np.sin(r0_lat) * np.sin(r1_lat) + np.cos(r0_lat) * np.cos( - r1_lat - ) * np.cos(delta_x) - try: - res = 1000 * np.arccos(cos_d) * self.GEO_R1 - return res - except: - traceback.print_exc() - # #print("cos_d =", cos_d) - # #print("parameter:", p0_lon, p0_lat, p1_lon, p1_lat) - return np.array([]) - - # return [m] - def get_dist_on_earth_hubeny(self, p0_lon, p0_lat, p1_lon, p1_lat): - if p0_lon == p1_lon and p0_lat == p1_lat: - return 0 - (r0_lon, r0_lat, r1_lon, r1_lat) = map( - math.radians, [p0_lon, p0_lat, p1_lon, p1_lat] - ) - lat_t = (r0_lat + r1_lat) / 2 - w = 1 - self.GEO_E2 * math.sin(lat_t) ** 2 - c2 = math.cos(lat_t) ** 2 - return math.sqrt( - (self.GEO_R2_2 / w**3) * (r0_lat - r1_lat) ** 2 - + (self.GEO_R1_2 / w) * c2 * (r0_lon - r1_lon) ** 2 - ) - - @staticmethod - def calc_azimuth(lat, lon): - rad_latitude = np.radians(lat) - rad_longitude = np.radians(lon) - rad_longitude_delta = rad_longitude[1:] - rad_longitude[0:-1] - azimuth = np.mod( - np.degrees( - np.arctan2( - np.sin(rad_longitude_delta), - np.cos(rad_latitude[0:-1]) * np.tan(rad_latitude[1:]) - - np.sin(rad_latitude[0:-1]) * np.cos(rad_longitude_delta), - ) - ), - 360, - ).astype(dtype="int16") - return azimuth - - @staticmethod - def get_maptile_filename(map_name, z, x, y): - return "maptile/" + map_name + "/{0}/{1}/{2}.png".format(z, x, y) - async def get_altitude_from_tile(self, pos): if np.isnan(pos[0]) or np.isnan(pos[1]): return np.nan z = self.G_DEM_MAP_CONFIG[self.G_DEM_MAP]["fix_zoomlevel"] - f_x, f_y, p_x, p_y = self.get_tilexy_and_xy_in_tile(z, pos[0], pos[1], 256) - filename = self.get_maptile_filename(self.G_DEM_MAP, z, f_x, f_y) + f_x, f_y, p_x, p_y = get_tilexy_and_xy_in_tile(z, pos[0], pos[1], 256) + filename = get_maptile_filename(self.G_DEM_MAP, z, f_x, f_y) if not os.path.exists(filename): await self.network.download_demtile(z, f_x, f_y) @@ -1305,30 +1194,6 @@ async def get_altitude_from_tile(self, pos): # print(altitude, filename, p_x, p_y, pos[1], pos[0]) return altitude - @staticmethod - def get_tilexy_and_xy_in_tile(z, x, y, tile_size): - n = 2.0**z - _y = math.radians(y) - x_in_tile, tile_x = math.modf((x + 180.0) / 360.0 * n) - y_in_tile, tile_y = math.modf( - (1.0 - math.log(math.tan(_y) + (1.0 / math.cos(_y))) / math.pi) / 2.0 * n - ) - - return ( - int(tile_x), - int(tile_y), - int(x_in_tile * tile_size), - int(y_in_tile * tile_size), - ) - - @staticmethod - def get_lon_lat_from_tile_xy(z, x, y): - n = 2.0**z - lon = x / n * 360.0 - 180.0 - lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n)))) - - return lon, lat - def get_courses(self): dirs = sorted( glob(os.path.join(self.G_COURSE_DIR, "*.tcx")), diff --git a/modules/course.py b/modules/course.py index 01f36b23..24ed013c 100644 --- a/modules/course.py +++ b/modules/course.py @@ -3,14 +3,14 @@ import re import shutil -from math import factorial - import oyaml import numpy as np from crdp import rdp from logger import app_logger from modules.loaders import TcxLoader +from modules.utils.filters import savitzky_golay +from modules.utils.geo import calc_azimuth, get_dist_on_earth, get_dist_on_earth_array from modules.utils.timer import Timer, log_timers POLYLINE_DECODER = False @@ -116,7 +116,7 @@ def reset(self, delete_course_file=False, replace=False): if os.path.exists(self.config.G_COURSE_FILE_PATH): os.remove(self.config.G_COURSE_FILE_PATH) if not replace and self.config.G_THINGSBOARD_API["STATUS"]: - self.config.network.api.send_livetrack_course_reset() + self.config.api.send_livetrack_course_reset() def load(self, file=None): # if file is given, copy it to self.config.G_COURSE_FILE_PATH firsthand, we are loading a new course @@ -177,7 +177,7 @@ def load(self, file=None): log_timers(timers, text_total="total : {0:.3f} sec") if self.config.G_THINGSBOARD_API["STATUS"]: - self.config.network.api.send_livetrack_course_load() + self.config.api.send_livetrack_course_load() async def load_google_map_route(self, load_html=False, html_file=None): self.reset() @@ -198,7 +198,7 @@ async def load_google_map_route(self, load_html=False, html_file=None): self.modify_course_points() if self.config.G_THINGSBOARD_API["STATUS"]: - self.config.network.api.send_livetrack_course_load() + self.config.api.send_livetrack_course_load() self.config.gui.init_course() @@ -215,7 +215,7 @@ async def search_route(self, x1, y1, x2, y2): self.modify_course_points() if self.config.G_THINGSBOARD_API["STATUS"]: - self.config.network.api.send_livetrack_course_load() + self.config.api.send_livetrack_course_load() def get_ridewithgps_privacycode(self, route_id): privacy_code = None @@ -232,7 +232,7 @@ def get_ridewithgps_privacycode(self, route_id): return privacy_code async def get_google_route_from_mapstogpx(self, url): - json_routes = await self.config.network.api.get_google_route_from_mapstogpx(url) + json_routes = await self.config.api.get_google_route_from_mapstogpx(url) self.info["Name"] = "Google routes" self.info["DistanceMeters"] = round(json_routes["totaldist"] / 1000, 1) @@ -316,7 +316,7 @@ async def get_google_route_from_mapstogpx(self, url): return async def get_google_route(self, x1, y1, x2, y2): - json_routes = await self.config.network.api.get_google_routes(x1, y1, x2, y2) + json_routes = await self.config.api.get_google_routes(x1, y1, x2, y2) if not POLYLINE_DECODER or json_routes is None or json_routes["status"] != "OK": return @@ -413,11 +413,12 @@ def downsample(self): self.altitude = self.altitude[cond] # [m] if len_dist: self.distance = self.distance[cond] / 1000 # [km] - except: + except Exception as e: # noqa + app_logger.warning(f"Error during downsampling: {e}") self.distance = self.distance / 1000 # [km] # for sensor_gps - self.azimuth = self.config.calc_azimuth(self.latitude, self.longitude) + self.azimuth = calc_azimuth(self.latitude, self.longitude) self.points_diff = np.array([np.diff(self.longitude), np.diff(self.latitude)]) self.points_diff_sum_of_squares = ( self.points_diff[0] ** 2 + self.points_diff[1] ** 2 @@ -426,7 +427,7 @@ def downsample(self): if not len_dist: self.distance = ( - self.config.get_dist_on_earth_array( + get_dist_on_earth_array( self.longitude[0:-1], self.latitude[0:-1], self.longitude[1:], @@ -439,7 +440,7 @@ def downsample(self): dist_diff = 1000 * np.diff(self.distance) # [m] if len_alt: - modified_altitude = self.savitzky_golay(self.altitude, 53, 3) + modified_altitude = savitzky_golay(self.altitude, 53, 3) # do not apply if length is different (occurs when too short course) if len(self.altitude) == len(modified_altitude): self.altitude = modified_altitude @@ -456,8 +457,10 @@ def downsample(self): diff_dist_max = int(np.max(dist_diff)) * 2 / 1000 # [m->km] if diff_dist_max > self.config.G_GPS_SEARCH_RANGE: # [km] + app_logger.info( + f"G_GPS_SEARCH_RANGE[km]: {self.config.G_GPS_SEARCH_RANGE} -> {diff_dist_max}" + ) self.config.G_GPS_SEARCH_RANGE = diff_dist_max - # print("G_GPS_SEARCH_RANGE[km]:", self.config.G_GPS_SEARCH_RANGE, diff_dist_max) app_logger.info(f"downsampling:{len_lat} -> {len(self.latitude)}") @@ -481,6 +484,7 @@ def calc_slope_smoothing(self): dist_diff[0, 1:] = self.distance[1:] - self.distance[0:-1] alt_diff[0, 1:] = self.altitude[1:] - self.altitude[0:-1] grade[0, 1:] = alt_diff[0, 1:] / (dist_diff[0, 1:] * 1000) * 100 + for i in range(1, diff_num): dist_diff[i, i:-i] = self.distance[2 * i :] - self.distance[0 : -2 * i] dist_diff[i, 0:i] = self.distance[i : 2 * i] - self.distance[0] @@ -492,11 +496,13 @@ def calc_slope_smoothing(self): grade_mod = np.zeros(course_n) cond_all = np.full(course_n, False) + for i in range(diff_num - 1): cond = dist_diff[i] >= self.config.G_CLIMB_DISTANCE_CUTOFF cond_diff = cond ^ cond_all grade_mod[cond_diff] = grade[i][cond_diff] cond_all = cond + cond = np.full(course_n, True) cond_diff = cond ^ cond_all grade_mod[cond_diff] = grade[3][cond_diff] @@ -505,11 +511,13 @@ def calc_slope_smoothing(self): self.slope_smoothing = np.zeros(course_n) self.slope_smoothing[0] = grade_mod[0] self.slope_smoothing[-1] = grade_mod[-1] + # forward for i in range(1, course_n - 1): self.slope_smoothing[i] = grade_mod[ i ] * LP_coefficient + self.slope_smoothing[i - 1] * (1 - LP_coefficient) + # backward for i in reversed(range(course_n - 1)): self.slope_smoothing[i] = self.slope_smoothing[ @@ -518,6 +526,7 @@ def calc_slope_smoothing(self): # detect climbs slope_smoothing_cat = np.zeros(course_n).astype("uint8") + for i in range(i, len(self.config.G_SLOPE_CUTOFF) - 1): slope_smoothing_cat = np.where( (self.config.G_SLOPE_CUTOFF[i - 1] < self.slope_smoothing) @@ -525,6 +534,7 @@ def calc_slope_smoothing(self): i, slope_smoothing_cat, ) + slope_smoothing_cat = np.where( (self.config.G_SLOPE_CUTOFF[-1] < self.slope_smoothing), len(self.config.G_SLOPE_CUTOFF) - 1, @@ -534,6 +544,7 @@ def calc_slope_smoothing(self): climb_search_state = False climb_start_cutoff = 2 climb_end_cutoff = 1 + if slope_smoothing_cat[0] >= climb_start_cutoff: self.climb_segment.append( { @@ -543,6 +554,7 @@ def calc_slope_smoothing(self): } ) climb_search_state = True + for i in range(1, course_n): # search climb end (detect top of climb) if ( @@ -620,194 +632,174 @@ def calc_slope_smoothing(self): self.colored_altitude = np.array(self.config.G_SLOPE_COLOR)[slope_smoothing_cat] def modify_course_points(self): - course_points = self.course_points + if self.config.G_COURSE_INDEXING: + course_points = self.course_points - len_pnt_lat = len(course_points.latitude) - len_pnt_dist = len(course_points.distance) - len_pnt_alt = len(course_points.altitude) - len_dist = len(self.distance) - len_alt = len(self.altitude) + len_pnt_lat = len(course_points.latitude) + len_pnt_dist = len(course_points.distance) + len_pnt_alt = len(course_points.altitude) + len_dist = len(self.distance) + len_alt = len(self.altitude) - # calculate course point distance - if not len_pnt_dist and len_dist: - course_points.distance = np.empty(len_pnt_lat) - if not len_pnt_alt and len_alt: - course_points.altitude = np.zeros(len_pnt_lat) - - min_index = 0 - for i in range(len_pnt_lat): - b_a_x = self.points_diff[0][min_index:] - b_a_y = self.points_diff[1][min_index:] - lon_diff = course_points.longitude[i] - self.longitude[min_index:] - lat_diff = course_points.latitude[i] - self.latitude[min_index:] - p_a_x = lon_diff[:-1] - p_a_y = lat_diff[:-1] - inner_p = (b_a_x * p_a_x + b_a_y * p_a_y) / self.points_diff_sum_of_squares[ - min_index: - ] - inner_p_check = np.where((0.0 <= inner_p) & (inner_p <= 1.0), True, False) - - min_j = None - min_dist_diff_h = np.inf - min_dist_delta = 0 - min_alt_delta = 0 - for j in list(*np.where(inner_p_check == True)): - h_lon = ( - self.longitude[min_index + j] - + ( - self.longitude[min_index + j + 1] - - self.longitude[min_index + j] - ) - * inner_p[j] - ) - h_lat = ( - self.latitude[min_index + j] - + (self.latitude[min_index + j + 1] - self.latitude[min_index + j]) - * inner_p[j] - ) - dist_diff_h = self.config.get_dist_on_earth( - h_lon, - h_lat, - course_points.longitude[i], - course_points.latitude[i], + # calculate course point distance + if not len_pnt_dist and len_dist: + course_points.distance = np.empty(len_pnt_lat) + if not len_pnt_alt and len_alt: + course_points.altitude = np.zeros(len_pnt_lat) + + min_index = 0 + for i in range(len_pnt_lat): + b_a_x = self.points_diff[0][min_index:] + b_a_y = self.points_diff[1][min_index:] + lon_diff = course_points.longitude[i] - self.longitude[min_index:] + lat_diff = course_points.latitude[i] - self.latitude[min_index:] + p_a_x = lon_diff[:-1] + p_a_y = lat_diff[:-1] + inner_p = ( + b_a_x * p_a_x + b_a_y * p_a_y + ) / self.points_diff_sum_of_squares[min_index:] + inner_p_check = np.where( + (0.0 <= inner_p) & (inner_p <= 1.0), True, False ) - if ( - dist_diff_h < self.config.G_GPS_ON_ROUTE_CUTOFF - and dist_diff_h < min_dist_diff_h - ): - if min_j is not None and j - min_j > 2: - continue - min_j = j - min_dist_diff_h = dist_diff_h - min_dist_delta = ( - self.config.get_dist_on_earth( - self.longitude[min_index + j], - self.latitude[min_index + j], - h_lon, - h_lat, + min_j = None + min_dist_diff_h = np.inf + min_dist_delta = 0 + min_alt_delta = 0 + for j in list(*np.where(inner_p_check == True)): + h_lon = ( + self.longitude[min_index + j] + + ( + self.longitude[min_index + j + 1] + - self.longitude[min_index + j] ) - / 1000 + * inner_p[j] ) - if len_alt: - min_alt_delta = ( - ( - self.altitude[min_index + j + 1] - - self.altitude[min_index + j] - ) - / ( - self.distance[min_index + j + 1] - - self.distance[min_index + j] + h_lat = ( + self.latitude[min_index + j] + + ( + self.latitude[min_index + j + 1] + - self.latitude[min_index + j] + ) + * inner_p[j] + ) + dist_diff_h = get_dist_on_earth( + h_lon, + h_lat, + course_points.longitude[i], + course_points.latitude[i], + ) + + if ( + dist_diff_h < self.config.G_GPS_ON_ROUTE_CUTOFF + and dist_diff_h < min_dist_diff_h + ): + if min_j is not None and j - min_j > 2: + continue + min_j = j + min_dist_diff_h = dist_diff_h + min_dist_delta = ( + get_dist_on_earth( + self.longitude[min_index + j], + self.latitude[min_index + j], + h_lon, + h_lat, ) - * min_dist_delta + / 1000 ) + if len_alt: + min_alt_delta = ( + ( + self.altitude[min_index + j + 1] + - self.altitude[min_index + j] + ) + / ( + self.distance[min_index + j + 1] + - self.distance[min_index + j] + ) + * min_dist_delta + ) - if min_j is None: - min_j = 0 - min_index = min_index + min_j + if min_j is None: + min_j = 0 + min_index = min_index + min_j - if not len_pnt_dist and len_dist: - course_points.distance[i] = self.distance[min_index] + min_dist_delta - if not len_pnt_alt and len_alt: - course_points.altitude[i] = self.altitude[min_index] + min_alt_delta - - # add climb tops - # if len(self.climb_segment): - # min_index = 0 - # for i in range(len(self.climb_segment)): - # diff_dist = np.abs(course_points.distance - self.climb_segment[i]['course_point_distance']) - # min_index = np.where(diff_dist == np.min(diff_dist))[0][0]+1 - # course_points.name.insert(min_index, "Top of Climb") - # course_points.latitude = np.insert(course_points._latitude, min_index, self.climb_segment[i]['course_point_latitude']) - # course_points.longitude = np.insert(course_points.longitude, min_index, self.climb_segment[i]['course_point_longitude']) - # course_points.type.insert(min_index, "Summit") - # course_points.distance = np.insert(course_points.distance, min_index, self.climb_segment[i]['course_point_distance']) - # course_points.altitude = np.insert(course_points.altitude, min_index, self.climb_segment[i]['course_point_altitude']) - - len_pnt_dist = len(course_points.distance) - len_pnt_alt = len(course_points.latitude) - - # add start course point - if ( - len_pnt_lat - and len_pnt_dist - and len_dist - # TODO do not use float - and course_points.distance[0] != 0.0 - ): - course_points.name = np.insert(course_points.name, 0, "Start") - course_points.latitude = np.insert( - course_points.latitude, 0, self.latitude[0] - ) - course_points.longitude = np.insert( - course_points.longitude, 0, self.longitude[0] - ) - course_points.type = np.insert(course_points.type, 0, "") - course_points.notes = np.insert(course_points.notes, 0, "") - if len_pnt_dist and len_dist: - course_points.distance = np.insert(course_points.distance, 0, 0.0) - if len_pnt_alt and len_alt: - course_points.altitude = np.insert( - course_points.altitude, 0, self.altitude[0] + if not len_pnt_dist and len_dist: + course_points.distance[i] = ( + self.distance[min_index] + min_dist_delta + ) + if not len_pnt_alt and len_alt: + course_points.altitude[i] = self.altitude[min_index] + min_alt_delta + + # add climb tops + # if len(self.climb_segment): + # min_index = 0 + # for i in range(len(self.climb_segment)): + # diff_dist = np.abs(course_points.distance - self.climb_segment[i]['course_point_distance']) + # min_index = np.where(diff_dist == np.min(diff_dist))[0][0]+1 + # course_points.name.insert(min_index, "Top of Climb") + # course_points.latitude = np.insert(course_points._latitude, min_index, self.climb_segment[i]['course_point_latitude']) + # course_points.longitude = np.insert(course_points.longitude, min_index, self.climb_segment[i]['course_point_longitude']) + # course_points.type.insert(min_index, "Summit") + # course_points.distance = np.insert(course_points.distance, min_index, self.climb_segment[i]['course_point_distance']) + # course_points.altitude = np.insert(course_points.altitude, min_index, self.climb_segment[i]['course_point_altitude']) + + len_pnt_dist = len(course_points.distance) + len_pnt_alt = len(course_points.latitude) + + # add start course point + if ( + len_pnt_lat + and len_pnt_dist + and len_dist + # TODO do not use float + and course_points.distance[0] != 0.0 + ): + course_points.name = np.insert(course_points.name, 0, "Start") + course_points.latitude = np.insert( + course_points.latitude, 0, self.latitude[0] ) - # add end course point - end_distance = None - if len(self.latitude) and len(course_points.longitude): - end_distance = self.config.get_dist_on_earth_array( - self.longitude[-1], - self.latitude[-1], - course_points.longitude[-1], - course_points.latitude[-1], - ) - if ( - len_pnt_lat - and len_pnt_dist - and len_dist - and end_distance is not None - and end_distance > 5 - ): - course_points.name = np.append(course_points.name, "End") - course_points.latitude = np.append( - course_points.latitude, self.latitude[-1] - ) - course_points.longitude = np.append( - course_points.longitude, self.longitude[-1] - ) - course_points.type = np.append(course_points.type, "") - course_points.notes = np.append(course_points.notes, "") - if len_pnt_dist and len_dist: - course_points.distance = np.append( - course_points.distance, self.distance[-1] + course_points.longitude = np.insert( + course_points.longitude, 0, self.longitude[0] ) - if len_pnt_alt and len_alt: - course_points.altitude = np.append( - course_points.altitude, self.altitude[-1] + course_points.type = np.insert(course_points.type, 0, "") + course_points.notes = np.insert(course_points.notes, 0, "") + if len_pnt_dist and len_dist: + course_points.distance = np.insert(course_points.distance, 0, 0.0) + if len_pnt_alt and len_alt: + course_points.altitude = np.insert( + course_points.altitude, 0, self.altitude[0] + ) + # add end course point + end_distance = None + if len(self.latitude) and len(course_points.longitude): + end_distance = get_dist_on_earth_array( + self.longitude[-1], + self.latitude[-1], + course_points.longitude[-1], + course_points.latitude[-1], ) - - @staticmethod - def savitzky_golay(y, window_size, order, deriv=0, rate=1): - try: - window_size = np.abs(np.intc(window_size)) - order = np.abs(np.intc(order)) - except ValueError: - raise ValueError("window_size and order have to be of type int") - if window_size % 2 != 1 or window_size < 1: - raise TypeError("window_size size must be a positive odd number") - if window_size < order + 2: - raise TypeError("window_size is too small for the polynomials order") - order_range = range(order + 1) - half_window = (window_size - 1) // 2 - # precompute coefficients - b = np.mat( - [ - [k**i for i in order_range] - for k in range(-half_window, half_window + 1) - ] - ) - m = np.linalg.pinv(b).A[deriv] * rate**deriv * factorial(deriv) - # pad the signal at the extremes with - # values taken from the signal itself - firstvals = y[0] - np.abs(y[1 : half_window + 1][::-1] - y[0]) - lastvals = y[-1] + np.abs(y[-half_window - 1 : -1][::-1] - y[-1]) - y = np.concatenate((firstvals, y, lastvals)) - return np.convolve(m[::-1], y, mode="valid") + if ( + len_pnt_lat + and len_pnt_dist + and len_dist + and end_distance is not None + and end_distance > 5 + ): + course_points.name = np.append(course_points.name, "End") + course_points.latitude = np.append( + course_points.latitude, self.latitude[-1] + ) + course_points.longitude = np.append( + course_points.longitude, self.longitude[-1] + ) + course_points.type = np.append(course_points.type, "") + course_points.notes = np.append(course_points.notes, "") + if len_pnt_dist and len_dist: + course_points.distance = np.append( + course_points.distance, self.distance[-1] + ) + if len_pnt_alt and len_alt: + course_points.altitude = np.append( + course_points.altitude, self.altitude[-1] + ) diff --git a/modules/display/pitft_28_r.py b/modules/display/pitft_28_r.py index 2e5723f5..0459b154 100644 --- a/modules/display/pitft_28_r.py +++ b/modules/display/pitft_28_r.py @@ -1,5 +1,5 @@ -from modules.utils.cmd import exec_cmd from logger import app_logger +from modules.utils.cmd import exec_cmd _SENSOR_DISPLAY = True diff --git a/modules/helper/api.py b/modules/helper/api.py index 147e99b2..02dfe097 100644 --- a/modules/helper/api.py +++ b/modules/helper/api.py @@ -66,9 +66,9 @@ async def get_google_routes(self, x1, y1, x2, y2): if np.any(np.isnan([x1, y1, x2, y2])): return None - origin = "origin={},{}".format(y1, x1) - destination = "destination={},{}".format(y2, x2) - language = "language={}".format(self.config.G_LANG) + origin = f"origin={y1},{x1}" + destination = f"destination={y2},{x2}" + language = f"language={self.config.G_LANG}" url = "{}&{}&key={}&{}&{}&{}".format( self.config.G_GOOGLE_DIRECTION_API["URL"], self.config.G_GOOGLE_DIRECTION_API["API_MODE"][ @@ -504,7 +504,7 @@ def send_livetrack_data(self, quick_send=False): return if self.config.G_THINGSBOARD_API["AUTO_UPLOAD_VIA_BT"] and self.bt_cmd_lock: - # print("[BT] {} locked, network status:{}, quick_send:{}".format(datetime.datetime.now().strftime("%H:%M:%S"), self.config.detect_network(), quick_send)) + # print("[BT] {} locked, network status:{}, quick_send:{}".format(datetime.datetime.now().strftime("%H:%M:%S"), detect_network(), quick_send)) return asyncio.create_task(self.send_livetrack_data_internal(quick_send)) @@ -539,7 +539,7 @@ async def send_livetrack_data_internal(self, quick_send=False): timestamp_str_log = datetime.datetime.fromtimestamp(t).strftime( "%m/%d %H:%M" ) - # print("[BT] {} start, network status:{}".format(timestamp_str, self.config.detect_network())) + # print("[BT] {} start, network status:{}".format(timestamp_str, detect_network())) # open connection if self.config.G_THINGSBOARD_API["AUTO_UPLOAD_VIA_BT"]: @@ -570,7 +570,7 @@ async def send_livetrack_data_internal(self, quick_send=False): datetime.datetime.now().strftime("%H:%M") + "CE" ) return - # print("[BT] {} connect, network status:{} {}".format(timestamp_str, self.config.detect_network(), count)) + # print("[BT] {} connect, network status:{} {}".format(timestamp_str, detect_network(), count)) await asyncio.sleep(5) @@ -597,11 +597,11 @@ async def send_livetrack_data_internal(self, quick_send=False): self.config.logger.sensor.values["integrated"]["accumulated_power"] / 1000 ), - #'w_prime_balance': self.config.logger.sensor.values['integrated']['w_prime_balance_normalized'], + # 'w_prime_balance': self.config.logger.sensor.values['integrated']['w_prime_balance_normalized'], "temperature": self.config.logger.sensor.values["integrated"][ "temperature" ], - #'altitude': self.config.logger.sensor.values['I2C']['altitude'], + # 'altitude': self.config.logger.sensor.values['I2C']['altitude'], "latitude": self.config.logger.sensor.values["GPS"]["lat"], "longitude": self.config.logger.sensor.values["GPS"]["lon"], }, diff --git a/modules/helper/network.py b/modules/helper/network.py index 2d22ddab..0cbf37bb 100644 --- a/modules/helper/network.py +++ b/modules/helper/network.py @@ -8,7 +8,7 @@ import aiofiles from logger import app_logger -from modules.helper.api import api +from modules.utils.map import get_maptile_filename from modules.utils.network import detect_network @@ -17,7 +17,6 @@ class Network: def __init__(self, config): self.config = config - self.api = api(self.config) self.download_queue = asyncio.Queue() asyncio.create_task(self.download_worker()) @@ -25,6 +24,24 @@ def __init__(self, config): async def quit(self): await self.download_queue.put(None) + @staticmethod + async def get_http_request(session, url, save_path, headers, params): + try: + async with session.get(url, headers=headers, params=params) as dl_file: + if dl_file.status == 200: + async with aiofiles.open(save_path, mode="wb") as f: + await f.write(await dl_file.read()) + return True + else: + app_logger.info( + f"dl_file status {dl_file.status}: {dl_file.reason}\n{url}" + ) + return False + except asyncio.CancelledError: + return False + except: + return False + @staticmethod async def get_json(url, params=None, headers=None, timeout=10): async with aiohttp.ClientSession() as session: @@ -43,6 +60,17 @@ async def post(url, headers=None, params=None, data=None): json = await res.json() return json + async def download_files(self, urls, save_paths, headers=None, params=None): + tasks = [] + async with asyncio.Semaphore(self.config.G_COROUTINE_SEM): + async with aiohttp.ClientSession() as session: + for url, save_path in zip(urls, save_paths): + tasks.append( + self.get_http_request(session, url, save_path, headers, params) + ) + res = await asyncio.gather(*tasks) + return res + async def download_worker(self): failed = [] # for urls, header, save_paths, params: @@ -79,6 +107,7 @@ async def download_worker(self): q["save_paths"] = retry_save_paths await self.download_queue.put(q) + # tiles functions async def download_maptile( self, map_config, map_name, z, tiles, additional_download=False ): @@ -129,13 +158,11 @@ async def download_maptile( request_header["User-Agent"] = map_config[map_name]["user_agent"] for tile in tiles: - os.makedirs( - "maptile/" + map_name + "/{0}/{1}/".format(z, tile[0]), exist_ok=True - ) + os.makedirs(f"maptile/{map_name}/{z}/{tile[0]}/", exist_ok=True) url = map_config[map_name]["url"].format( z=z, x=tile[0], y=tile[1], **additional_var ) - save_path = self.config.get_maptile_filename(map_name, z, *tile) + save_path = get_maptile_filename(map_name, z, *tile) urls.append(url) save_paths.append(save_path) @@ -143,29 +170,28 @@ async def download_maptile( {"urls": urls, "headers": request_header, "save_paths": save_paths} ) - max_zoom_cond = True - if ( - "max_zoomlevel" in map_config[map_name] - and z + 1 >= map_config[map_name]["max_zoomlevel"] - ): - max_zoom_cond = False - min_zoom_cond = True - if ( - "min_zoomlevel" in map_config[map_name] - and z - 1 <= map_config[map_name]["min_zoomlevel"] - ): - min_zoom_cond = False - if additional_download: additional_urls = [] additional_save_paths = [] + + max_zoom_cond = True + if ( + "max_zoomlevel" in map_config[map_name] + and z + 1 >= map_config[map_name]["max_zoomlevel"] + ): + max_zoom_cond = False + min_zoom_cond = True + if ( + "min_zoomlevel" in map_config[map_name] + and z - 1 <= map_config[map_name]["min_zoomlevel"] + ): + min_zoom_cond = False + for tile in tiles: if max_zoom_cond: for i in range(2): os.makedirs( - "maptile/" - + map_name - + "/{0}/{1}/".format(z + 1, 2 * tile[0] + i), + f"maptile/{map_name}/{z + 1}/{2 * tile[0] + i}", exist_ok=True, ) for j in range(2): @@ -173,9 +199,9 @@ async def download_maptile( z=z + 1, x=2 * tile[0] + i, y=2 * tile[1] + j, - **additional_var + **additional_var, ) - save_path = self.config.get_maptile_filename( + save_path = get_maptile_filename( map_name, z + 1, 2 * tile[0] + i, 2 * tile[1] + j ) additional_urls.append(url) @@ -186,26 +212,24 @@ async def download_maptile( if min_zoom_cond: os.makedirs( - "maptile/" - + map_name - + "/{0}/{1}/".format(z - 1, int(tile[0] / 2)), + f"maptile/{map_name}/{z - 1}/{int(tile[0] / 2)}", exist_ok=True, ) zoomout_url = map_config[map_name]["url"].format( z=z - 1, x=int(tile[0] / 2), y=int(tile[1] / 2), - **additional_var + **additional_var, ) if zoomout_url not in additional_urls: additional_urls.append(zoomout_url) additional_save_paths.append( - self.config.get_maptile_filename( + get_maptile_filename( map_name, z - 1, int(tile[0] / 2), int(tile[1] / 2) ) ) - if len(additional_urls): + if additional_urls: await self.download_queue.put( { "urls": additional_urls, @@ -216,39 +240,12 @@ async def download_maptile( return True - @staticmethod - async def get_http_request(session, url, save_path, headers, params): - try: - async with session.get(url, headers=headers, params=params) as dl_file: - if dl_file.status == 200: - async with aiofiles.open(save_path, mode="wb") as f: - await f.write(await dl_file.read()) - return True - else: - return False - except asyncio.CancelledError: - return False - except: - return False - - async def download_files(self, urls, save_paths, headers=None, params=None): - tasks = [] - async with asyncio.Semaphore(self.config.G_COROUTINE_SEM): - async with aiohttp.ClientSession() as session: - for url, save_path in zip(urls, save_paths): - tasks.append( - self.get_http_request(session, url, save_path, headers, params) - ) - res = await asyncio.gather(*tasks) - return res - async def download_demtile(self, z, x, y): if not detect_network(): return False - header = {} try: os.makedirs( - "maptile/" + self.config.G_DEM_MAP + "/{0}/{1}/".format(z, x), + f"maptile/{self.config.G_DEM_MAP}/{z}/{x}/", exist_ok=True, ) await self.download_queue.put( @@ -258,11 +255,8 @@ async def download_demtile(self, z, x, y): "url" ].format(z=z, x=x, y=y), ], - "headers": header, "save_paths": [ - self.config.get_maptile_filename( - self.config.G_DEM_MAP, z, x, y - ), + get_maptile_filename(self.config.G_DEM_MAP, z, x, y), ], } ) diff --git a/modules/loaders/__init__.py b/modules/loaders/__init__.py index 67a1d1af..acba6447 100644 --- a/modules/loaders/__init__.py +++ b/modules/loaders/__init__.py @@ -1 +1 @@ -from .tcx import TcxLoader +from .tcx import TcxLoader # noqa diff --git a/modules/logger/logger_fit.py b/modules/logger/logger_fit.py index a0deadbb..0b0113fb 100644 --- a/modules/logger/logger_fit.py +++ b/modules/logger/logger_fit.py @@ -1,8 +1,8 @@ -import datetime import os import sqlite3 import struct import time +from datetime import datetime, timedelta from logger import app_logger from modules.utils.date import datetime_myparser @@ -35,7 +35,7 @@ class config_local: class LoggerFit(Logger): mode = None - epoch_datetime = datetime.datetime(1989, 12, 31, 0, 0, 0, 0) + epoch_datetime = datetime(1989, 12, 31, 0, 0, 0, 0) profile = { 0: { "name": "file_id", @@ -350,7 +350,7 @@ def write_log_python(self): l_num_used = True if l_num == -1: l_num_used = False - # write header if need + # write header if needed local_message_num = (local_message_num + 1) % 16 self.local_num[local_message_num] = { "message_num": message_num, @@ -411,7 +411,7 @@ def write_log_python(self): # write fit file ################ - startdate_local = start_date + datetime.timedelta(seconds=offset) + startdate_local = start_date + timedelta(seconds=offset) self.config.G_LOG_START_DATE = startdate_local.strftime("%Y%m%d%H%M%S") filename = os.path.join( self.config.G_LOG_DIR, f"{self.config.G_LOG_START_DATE}.fit" diff --git a/modules/logger_core.py b/modules/logger_core.py index de3ebfc0..100ba20b 100644 --- a/modules/logger_core.py +++ b/modules/logger_core.py @@ -10,9 +10,9 @@ import numpy as np from crdp import rdp +from logger import app_logger from modules.utils.cmd import exec_cmd from modules.utils.date import datetime_myparser -from logger import app_logger class LoggerCore: @@ -99,8 +99,7 @@ def start_coroutine(self): def delay_init(self): from .course import Course - from .logger import logger_csv - from .logger import logger_fit + from .logger import logger_csv, logger_fit from . import sensor_core self.sensor = sensor_core.SensorCore(self.config) @@ -343,7 +342,7 @@ def start_and_stop_manual(self): # send online if self.config.G_THINGSBOARD_API["STATUS"]: - self.config.network.api.send_livetrack_data(quick_send=True) + self.config.api.send_livetrack_data(quick_send=True) # show message self.config.gui.show_popup(self.config.G_MANUAL_STATUS + popup_extra) @@ -424,7 +423,7 @@ def reset_count(self): if not self.logger_fit.write_log(): return app_logger.info( - f"Write Fit({self.logger_fit.mode}) : {(datetime.datetime.now() - t).total_seconds()} sec" + f"Write fit({self.logger_fit.mode}) : {(datetime.datetime.now() - t).total_seconds()} sec" ) # backup and reset database @@ -659,7 +658,7 @@ async def record_log(self): # send online if self.config.G_THINGSBOARD_API["STATUS"]: - self.config.network.api.send_livetrack_data(quick_send=False) + self.config.api.send_livetrack_data(quick_send=False) def calc_gross(self): # elapsed_time diff --git a/modules/pyqt/graph/pyqt_base_map.py b/modules/pyqt/graph/pyqt_base_map.py index c1175afb..350b66fe 100644 --- a/modules/pyqt/graph/pyqt_base_map.py +++ b/modules/pyqt/graph/pyqt_base_map.py @@ -20,9 +20,6 @@ class BaseMapWidget(ScreenWidget): # load course course_loaded = False - # course points - course_points_label = [] - # signal for physical button signal_move_x_plus = QtCore.pyqtSignal() signal_move_x_minus = QtCore.pyqtSignal() diff --git a/modules/pyqt/graph/pyqt_map.py b/modules/pyqt/graph/pyqt_map.py index 5a604aed..03aeb606 100644 --- a/modules/pyqt/graph/pyqt_map.py +++ b/modules/pyqt/graph/pyqt_map.py @@ -1,8 +1,8 @@ -import os import datetime -import sqlite3 import io import math +import os +import sqlite3 import numpy as np from PIL import Image @@ -11,6 +11,18 @@ from modules._pyqt import QtCore, QtGui, pg, qasync from modules.pyqt.pyqt_cuesheet_widget import CueSheetWidget from modules.pyqt.graph.pyqtgraph.CoursePlotItem import CoursePlotItem +from modules.utils.geo import ( + calc_y_mod, + get_mod_lat, + get_mod_lat_np, + get_width_distance, +) +from modules.utils.map import ( + get_maptile_filename, + get_lon_lat_from_tile_xy, + get_tilexy_and_xy_in_tile, + remove_maptiles, +) from modules.utils.timer import Timer, log_timers from .pyqt_base_map import BaseMapWidget @@ -90,6 +102,7 @@ def setup_ui_extra(self): color=(0, 0, 0), ) self.map_attribution.setZValue(100) + self.plot.addItem(self.map_attribution) # current point @@ -237,9 +250,8 @@ def init_cuesheet_and_instruction(self): if self.cuesheet_widget is None: self.cuesheet_widget = CueSheetWidget(self, self.config) self.cuesheet_widget.hide() # adhoc - # self.map_cuesheet_ratio = 0.7 + self.map_cuesheet_ratio = 1.0 - # self.layout.addWidget(self.cuesheet_widget, 0, 3, 4, 4) # init instruction self.instruction = pg.TextItem( @@ -300,7 +312,7 @@ def load_course(self): else: self.course_plot = CoursePlotItem( x=course.longitude, - y=self.get_mod_lat_np(course.latitude), + y=get_mod_lat_np(course.latitude), brushes=course.colored_altitude, width=6, ) @@ -318,7 +330,7 @@ def load_course(self): p = { "pos": [ course.longitude[i], - self.get_mod_lat(course.latitude[i]), + get_mod_lat(course.latitude[i]), ], "size": 2, "pen": {"color": "w", "width": 1}, @@ -355,7 +367,7 @@ def load_course(self): cp = { "pos": [ course_points.longitude[i], - self.get_mod_lat(course_points.latitude[i]), + get_mod_lat(course_points.latitude[i]), ], "pen": {"color": color, "width": 1}, "symbol": symbol, @@ -398,7 +410,7 @@ async def update_extra(self): self.config.G_DUMMY_POS_Y, ] # update y_mod (adjust for lat:lon=1:1) - self.y_mod = self.calc_y_mod(self.point["pos"][1]) + self.y_mod = calc_y_mod(self.point["pos"][1]) # add position circle to map if self.gps_values["mode"] == 3: self.point["brush"] = self.point_color["fix"] @@ -425,7 +437,7 @@ async def update_extra(self): index = self.gps_sensor.get_index_with_distance_cutoff( self.gps_values["course_index"], # get some forward distance [m] - self.get_width_distance(self.map_pos["y"], self.map_area["w"]) / 1000, + get_width_distance(self.map_pos["y"], self.map_area["w"]) / 1000, ) x2 = course.longitude[index] y2 = course.latitude[index] @@ -485,7 +497,7 @@ async def update_extra(self): else: self.center_point_data["size"] = 15 self.center_point_data["pos"][0] = self.map_pos["x"] - self.center_point_data["pos"][1] = self.get_mod_lat(self.map_pos["y"]) + self.center_point_data["pos"][1] = get_mod_lat(self.map_pos["y"]) self.center_point_location.append(self.center_point_data) self.center_point.setData(self.center_point_location) self.plot.addItem(self.center_point) @@ -499,9 +511,7 @@ async def update_extra(self): if not np.isnan(x_start) and not np.isnan(x_end): self.plot.setXRange(x_start, x_end, padding=0) if not np.isnan(y_start) and not np.isnan(y_end): - self.plot.setYRange( - self.get_mod_lat(y_start), self.get_mod_lat(y_end), padding=0 - ) + self.plot.setYRange(get_mod_lat(y_start), get_mod_lat(y_end), padding=0) if not np.any(np.isnan([x_start, x_end, y_start, y_end])): await self.draw_map_tile(x_start, x_end, y_start, y_end) @@ -534,9 +544,7 @@ def get_track(self): self.tracks_lon_pos = lon[-1] self.tracks_lat_pos = lat[-1] self.tracks_lon = np.append(self.tracks_lon, np.array(lon)) - self.tracks_lat = np.append( - self.tracks_lat, self.get_mod_lat_np(np.array(lat)) - ) + self.tracks_lat = np.append(self.tracks_lat, get_mod_lat_np(np.array(lat))) def reset_track(self): self.tracks_lon = np.array([]) @@ -585,7 +593,7 @@ async def draw_map_tile(self, x_start, x_end, y_start, y_end): self.zoomlevel, p0, p1, - overley=False, + overlay=False, use_mbtiles=self.config.G_MAP_CONFIG[self.config.G_MAP]["use_mbtiles"], ) @@ -636,13 +644,13 @@ async def overlay_windmap(self, drawn_main_map, p0, p1): if self.update_overlay_basetime(map_config, map_name): # basetime update if "jpn_scw" in map_name: - init_time_list = await self.config.network.api.get_scw_list( + init_time_list = await self.config.api.get_scw_list( map_config[map_name], "inittime" ) if init_time_list is not None: map_config[map_name]["basetime"] = init_time_list[0]["it"] - timeline = await self.config.network.api.get_scw_list( + timeline = await self.config.api.get_scw_list( map_config[map_name], "fl" ) if timeline is not None: @@ -686,7 +694,7 @@ def update_overlay_basetime(self, map_config, map_name): self.drawn_tile[map_name] = {} self.existing_tiles[map_name] = {} self.pre_zoomlevel[map_name] = np.nan - self.config.remove_maptiles(map_name) + remove_maptiles(map_name) config["nowtime"] = nowtime_mod return True @@ -712,12 +720,12 @@ async def overlay_map(self, drawn_main_map, p0, p1, map_config, map_name): <= map_config[map_name]["max_zoomlevel"] ): await self.draw_map_tile_by_overlay( - map_config, map_name, z, p0, p1, overley=True + map_config, map_name, z, p0, p1, overlay=True ) # above maximum zoom level: expand max zoomlevel tiles elif z > map_config[map_name]["max_zoomlevel"]: await self.draw_map_tile_by_overlay( - map_config, map_name, z, p0, p1, overley=True, expand=True + map_config, map_name, z, p0, p1, overlay=True, expand=True ) else: self.pre_zoomlevel[map_name] = z @@ -729,7 +737,7 @@ async def draw_map_tile_by_overlay( z, p0, p1, - overley=False, + overlay=False, expand=False, use_mbtiles=False, ): @@ -755,7 +763,7 @@ async def draw_map_tile_by_overlay( # tile check if use_mbtiles: self.con = sqlite3.connect( - "file:./maptile/{0:^s}.mbtiles?mode=ro".format(map_name), uri=True + f"file:./maptile/{map_name}.mbtiles?mode=ro", uri=True ) self.cur = self.con.cursor() @@ -793,12 +801,12 @@ async def draw_map_tile_by_overlay( ) imgitem = pg.ImageItem(imgarray) - if overley: + if overlay: imgitem.setCompositionMode(QtGui.QPainter.CompositionMode_Darken) - imgarray_min_x, imgarray_max_y = self.config.get_lon_lat_from_tile_xy( + imgarray_min_x, imgarray_max_y = get_lon_lat_from_tile_xy( z, keys[0], keys[1] ) - imgarray_max_x, imgarray_min_y = self.config.get_lon_lat_from_tile_xy( + imgarray_max_x, imgarray_min_y = get_lon_lat_from_tile_xy( z, keys[0] + 1, keys[1] + 1 ) @@ -807,9 +815,9 @@ async def draw_map_tile_by_overlay( imgitem.setRect( pg.QtCore.QRectF( imgarray_min_x, - self.get_mod_lat(imgarray_min_y), + get_mod_lat(imgarray_min_y), imgarray_max_x - imgarray_min_x, - self.get_mod_lat(imgarray_max_y) - self.get_mod_lat(imgarray_min_y), + get_mod_lat(imgarray_max_y) - get_mod_lat(imgarray_min_y), ) ) if use_mbtiles: @@ -818,7 +826,8 @@ async def draw_map_tile_by_overlay( return True - def init_draw_map(self, map_config, map_name, z, p0, p1, expand, tile_size): + @staticmethod + def init_draw_map(map_config, map_name, z, p0, p1, expand, tile_size): z_draw = z z_conv_factor = 1 if expand: @@ -830,8 +839,8 @@ def init_draw_map(self, map_config, map_name, z, p0, p1, expand, tile_size): z_conv_factor = 2 ** (z - z_draw) # tile range - t0 = self.config.get_tilexy_and_xy_in_tile(z, p0["x"], p0["y"], tile_size) - t1 = self.config.get_tilexy_and_xy_in_tile(z, p1["x"], p1["y"], tile_size) + t0 = get_tilexy_and_xy_in_tile(z, p0["x"], p0["y"], tile_size) + t1 = get_tilexy_and_xy_in_tile(z, p1["x"], p1["y"], tile_size) tile_x = sorted([t0[0], t1[0]]) tile_y = sorted([t0[1], t1[1]]) return z_draw, z_conv_factor, tile_x, tile_y @@ -866,7 +875,7 @@ def get_tiles_for_drawing(tile_x, tile_y, z_conv_factor, expand): async def download_tiles(self, tiles, map_config, map_name, z_draw): download_tile = [] for tile in tiles: - filename = self.config.get_maptile_filename(map_name, z_draw, *tile) + filename = get_maptile_filename(map_name, z_draw, *tile) key = "{0}-{1}".format(*tile) if os.path.exists(filename) and os.path.getsize(filename) > 0: @@ -930,8 +939,9 @@ def check_tile(self, use_mbtiles, map_name, z_draw, key): if (exist_tile_key, True) in self.existing_tiles[map_name][z_draw].items(): cond = True else: - sql = "select count(*) from tiles where zoom_level={} and tile_column={} and tile_row={}".format( - z_draw, key[0], 2**z_draw - 1 - key[1] + sql = ( + f"select count(*) from tiles where " + f"zoom_level={z_draw} and tile_column={key[0]} and tile_row={2**z_draw - 1 - key[1]}" ) if (self.cur.execute(sql).fetchone())[0] == 1: cond = True @@ -939,10 +949,11 @@ def check_tile(self, use_mbtiles, map_name, z_draw, key): def get_image_file(self, use_mbtiles, map_name, z_draw, x, y): if not use_mbtiles: - img_file = self.config.get_maptile_filename(map_name, z_draw, x, y) + img_file = get_maptile_filename(map_name, z_draw, x, y) else: - sql = "select tile_data from tiles where zoom_level={} and tile_column={} and tile_row={}".format( - z_draw, x, 2**z_draw - 1 - y + sql = ( + f"select tile_data from tiles where " + f"zoom_level={z_draw} and tile_column={x} and tile_row={2 ** z_draw - 1 - y}" ) img_file = io.BytesIO((self.cur.execute(sql).fetchone())[0]) return img_file @@ -950,7 +961,7 @@ def get_image_file(self, use_mbtiles, map_name, z_draw, x, y): def draw_scale(self, x_start, y_start): # draw scale at left bottom scale_factor = 10 - scale_dist = self.get_width_distance(y_start, self.map_area["w"]) / scale_factor + scale_dist = get_width_distance(y_start, self.map_area["w"]) / scale_factor num = scale_dist / (10 ** int(np.log10(scale_dist))) modify = 1 if 1 < num < 2: @@ -963,8 +974,8 @@ def draw_scale(self, x_start, y_start): scale_x2 = scale_x1 + self.map_area["w"] / scale_factor * modify scale_y1 = y_start + self.map_area["h"] / 25 scale_y2 = scale_y1 + self.map_area["h"] / 30 - scale_y1 = self.get_mod_lat(scale_y1) - scale_y2 = self.get_mod_lat(scale_y2) + scale_y1 = get_mod_lat(scale_y1) + scale_y2 = get_mod_lat(scale_y2) self.scale_plot.setData( [scale_x1, scale_x1, scale_x2, scale_x2], [scale_y2, scale_y1, scale_y1, scale_y2], @@ -975,16 +986,12 @@ def draw_scale(self, x_start, y_start): if scale_label >= 1000: scale_label = int(scale_label / 1000) scale_unit = "km" - self.scale_text.setPlainText( - "{0}{1}\n(z{2})".format(scale_label, scale_unit, self.zoomlevel) - ) + self.scale_text.setPlainText(f"{scale_label}{scale_unit}\n(z{self.zoomlevel})") self.scale_text.setPos((scale_x1 + scale_x2) / 2, scale_y2) def draw_map_attribution(self, x_start, y_start): # draw map attribution at right bottom - self.map_attribution.setPos( - x_start + self.map_area["w"], self.get_mod_lat(y_start) - ) + self.map_attribution.setPos(x_start + self.map_area["w"], get_mod_lat(y_start)) async def update_cuesheet_and_instruction( self, x_start, x_end, y_start, y_end, auto_zoom=False @@ -1015,10 +1022,7 @@ async def update_cuesheet_and_instruction( ) self.instruction.setPos( (x_end + x_start) / 2, - ( - self.get_mod_lat(y_start) - + (self.get_mod_lat(y_end) - self.get_mod_lat(y_start)) * 0.85 - ), + (get_mod_lat(y_start) + (get_mod_lat(y_end) - get_mod_lat(y_start)) * 0.85), ) self.plot.addItem(self.instruction) @@ -1042,43 +1046,17 @@ async def update_cuesheet_and_instruction( # print("zoom out", self.auto_zoomlevel_back, self.zoomlevel) self.auto_zoomlevel_back = None - def calc_y_mod(self, lat): - if np.isnan(lat): - return np.nan - return self.config.GEO_R2 / (self.config.GEO_R1 * math.cos(lat / 180 * np.pi)) - - def get_width_distance(self, lat, w): - return ( - w - * self.config.GEO_R1 - * 1000 - * 2 - * np.pi - * math.cos(lat / 180 * np.pi) - / 360 - ) - - def get_mod_lat(self, lat): - return lat * self.calc_y_mod(lat) - - def get_mod_lat_np(self, lat): - return ( - lat * self.config.GEO_R2 / (self.config.GEO_R1 * np.cos(lat / 180 * np.pi)) - ) - def get_geo_area(self, x, y): if np.isnan(x) or np.isnan(y): return np.nan, np.nan - tile_x, tile_y, _, _ = self.config.get_tilexy_and_xy_in_tile( + tile_x, tile_y, _, _ = get_tilexy_and_xy_in_tile( self.zoomlevel, x, y, self.config.G_MAP_CONFIG[self.config.G_MAP]["tile_size"], ) - pos_x0, pos_y0 = self.config.get_lon_lat_from_tile_xy( - self.zoomlevel, tile_x, tile_y - ) - pos_x1, pos_y1 = self.config.get_lon_lat_from_tile_xy( + pos_x0, pos_y0 = get_lon_lat_from_tile_xy(self.zoomlevel, tile_x, tile_y) + pos_x1, pos_y1 = get_lon_lat_from_tile_xy( self.zoomlevel, tile_x + 1, tile_y + 1 ) return ( diff --git a/modules/pyqt/menu/pyqt_course_menu_widget.py b/modules/pyqt/menu/pyqt_course_menu_widget.py index 92d6d6d6..c44f13d1 100644 --- a/modules/pyqt/menu/pyqt_course_menu_widget.py +++ b/modules/pyqt/menu/pyqt_course_menu_widget.py @@ -220,7 +220,7 @@ async def list_local_courses(self): self.add_list_item(course_item) async def list_ride_with_gps(self, add=False, reset=False): - courses = await self.config.network.api.get_ridewithgps_route(add, reset) + courses = await self.config.api.get_ridewithgps_route(add, reset) for c in reversed(courses or []): course_item = CourseListItemWidget(self, self.list_type, c) @@ -382,7 +382,7 @@ async def load_images(self): return else: # 1st download - await self.config.network.api.get_ridewithgps_files(self.list_id) + await self.config.api.get_ridewithgps_files(self.list_id) def on_back_menu(self): self.timer.stop() @@ -395,9 +395,8 @@ async def update_display(self): # sequentially draw with download # 1st download check - if ( - self.privacy_code is None - and self.config.network.api.check_ridewithgps_files(self.list_id, "1st") + if self.privacy_code is None and self.config.api.check_ridewithgps_files( + self.list_id, "1st" ): self.draw_images(draw_map_image=True, draw_profile_image=False) self.privacy_code = self.config.logger.course.get_ridewithgps_privacycode( @@ -405,13 +404,12 @@ async def update_display(self): ) if self.privacy_code is not None: # download files with privacy code (2nd download) - await self.config.network.api.get_ridewithgps_files_with_privacy_code( + await self.config.api.get_ridewithgps_files_with_privacy_code( self.list_id, self.privacy_code ) # 2nd download with privacy_code check - elif ( - self.privacy_code is not None - and self.config.network.api.check_ridewithgps_files(self.list_id, "2nd") + elif self.privacy_code is not None and self.config.api.check_ridewithgps_files( + self.list_id, "2nd" ): self.draw_images(draw_map_image=False, draw_profile_image=True) self.enable_next_button() @@ -419,8 +417,8 @@ async def update_display(self): def check_all_image_and_draw(self): # if all files exists, reload images and buttons, stop timer and exit - if not self.all_downloaded and self.config.network is not None: - self.all_downloaded = self.config.network.api.check_ridewithgps_files( + if not self.all_downloaded and self.config.api is not None: + self.all_downloaded = self.config.api.check_ridewithgps_files( self.list_id, "ALL" ) if self.all_downloaded: diff --git a/modules/pyqt/menu/pyqt_map_menu_widget.py b/modules/pyqt/menu/pyqt_map_menu_widget.py index 53976fff..30b13ea2 100644 --- a/modules/pyqt/menu/pyqt_map_menu_widget.py +++ b/modules/pyqt/menu/pyqt_map_menu_widget.py @@ -110,7 +110,7 @@ async def button_func_extra(self): # update strava cookie if "strava_heatmap" in self.config.G_HEATMAP_OVERLAY_MAP: asyncio.get_running_loop().run_in_executor( - None, self.config.network.api.get_strava_cookie + None, self.config.api.get_strava_cookie ) diff --git a/modules/pyqt/menu/pyqt_menu_widget.py b/modules/pyqt/menu/pyqt_menu_widget.py index 3a728ccd..d2a91caf 100644 --- a/modules/pyqt/menu/pyqt_menu_widget.py +++ b/modules/pyqt/menu/pyqt_menu_widget.py @@ -374,15 +374,15 @@ def setup_menu(self): @qasync.asyncSlot() async def strava_upload(self): - await self.buttons["Strava"].run(self.config.network.api.strava_upload) + await self.buttons["Strava"].run(self.config.api.strava_upload) @qasync.asyncSlot() async def garmin_upload(self): - await self.buttons["Garmin"].run(self.config.network.api.garmin_upload) + await self.buttons["Garmin"].run(self.config.api.garmin_upload) @qasync.asyncSlot() async def rwgps_upload(self): - await self.buttons["Ride with GPS"].run(self.config.network.api.rwgps_upload) + await self.buttons["Ride with GPS"].run(self.config.api.rwgps_upload) class LiveTrackMenuWidget(MenuWidget): @@ -400,7 +400,7 @@ def setup_menu(self): self.add_buttons(button_conf) if ( - self.config.network.api.thingsboard_check() + self.config.api.thingsboard_check() and self.config.G_THINGSBOARD_API["HAVE_API_TOKEN"] ): if not self.config.G_IS_RASPI: diff --git a/modules/sensor/sensor_gps.py b/modules/sensor/sensor_gps.py index ce04895d..1ad090fd 100644 --- a/modules/sensor/sensor_gps.py +++ b/modules/sensor/sensor_gps.py @@ -5,6 +5,7 @@ import numpy as np from modules.utils.cmd import exec_cmd, exec_cmd_return_value +from modules.utils.geo import calc_azimuth, get_dist_on_earth, get_track_str from logger import app_logger from .sensor import Sensor @@ -245,9 +246,7 @@ async def dummy_update(self): course_i = course_i % course_n lat_points = np.array([self.values["pre_lat"], self.values["lat"]]) lon_points = np.array([self.values["pre_lon"], self.values["lon"]]) - self.values["track"] = int( - (self.config.calc_azimuth(lat_points, lon_points))[0] - ) + self.values["track"] = int((calc_azimuth(lat_points, lon_points))[0]) # calculate course_index separately # t2 = datetime.datetime.utcnow() @@ -505,7 +504,7 @@ async def get_GPS_basic_values(self, lat, lon, alt, speed, track, mode, error, d ) ): # 2D distance : (x1, y1), (x2, y2) - dist = self.config.get_dist_on_earth( + dist = get_dist_on_earth( self.values["pre_lon"], self.values["pre_lat"], self.values["lon"], @@ -531,7 +530,7 @@ async def get_GPS_basic_values(self, lat, lon, alt, speed, track, mode, error, d and self.values["speed"] > self.config.G_GPS_SPEED_CUTOFF ): self.values["track"] = int(track) - self.values["track_str"] = self.config.get_track_str(self.values["track"]) + self.values["track_str"] = get_track_str(self.values["track"]) else: self.values["track"] = self.values["pre_track"] @@ -804,7 +803,7 @@ def get_course_index(self): course.latitude[m] + (course.latitude[m + 1] - course.latitude[m]) * inner_p[m] ) - dist_diff_h = self.config.get_dist_on_earth( + dist_diff_h = get_dist_on_earth( h_lon, h_lat, self.values["lon"], self.values["lat"] ) @@ -833,7 +832,7 @@ def get_course_index(self): continue self.values["on_course_status"] = True - dist_diff_course = self.config.get_dist_on_earth( + dist_diff_course = get_dist_on_earth( course.longitude[m], course.latitude[m], self.values["lon"], diff --git a/modules/sensor/sensor_i2c.py b/modules/sensor/sensor_i2c.py index b7163c11..5660618a 100644 --- a/modules/sensor/sensor_i2c.py +++ b/modules/sensor/sensor_i2c.py @@ -4,8 +4,10 @@ import numpy as np -from modules.utils.network import detect_network from logger import app_logger +from modules.utils.filters import KalmanFilter, KalmanFilter_pitch +from modules.utils.geo import get_track_str +from modules.utils.network import detect_network from .sensor import Sensor # I2C @@ -34,9 +36,6 @@ G = 9.80665 -# kalman filter -from .kalman_filter import KalmanFilter, KalmanFilter_pitch - class SensorI2C(Sensor): sensor = {} @@ -436,10 +435,13 @@ def start_coroutine(self): asyncio.create_task(self.start()) async def start(self): - while not self.config.G_QUIT: - await self.sleep() - await self.update() - self.get_sleep_time(self.config.G_I2C_INTERVAL) + try: + while True: + await self.sleep() + await self.update() + self.get_sleep_time(self.config.G_I2C_INTERVAL) + except asyncio.CancelledError: + pass async def update(self): # timestamp @@ -796,7 +798,7 @@ def calc_heading(self, yaw): self.values["heading"] = ( int(math.degrees(tilt_heading)) - self.config.G_IMU_MAG_DECLINATION ) - self.values["heading_str"] = self.config.get_track_str(self.values["heading"]) + self.values["heading_str"] = get_track_str(self.values["heading"]) @staticmethod def get_pitch_roll(acc): @@ -1105,7 +1107,7 @@ async def update_sealevel_pa(self, alt): else: v = self.config.logger.sensor.values["GPS"] try: - api_data = await self.config.network.api.get_openweathermap_data( + api_data = await self.config.api.get_openweathermap_data( v["lon"], v["lat"] ) if api_data is None: diff --git a/modules/sensor_core.py b/modules/sensor_core.py index 7e4f6036..ef1045c9 100644 --- a/modules/sensor_core.py +++ b/modules/sensor_core.py @@ -104,7 +104,7 @@ def __init__(self, config): for s in self.average_secs: for v in self.average_values: self.average_values[v][s] = [] - self.values["integrated"]["ave_{}_{}s".format(v, s)] = np.nan + self.values["integrated"][f"ave_{v}_{s}s"] = np.nan if _IMPORT_PSUTIL: self.process = psutil.Process(self.config.G_PID) @@ -155,475 +155,488 @@ async def integrate(self): self.wait_time = self.config.G_SENSOR_INTERVAL self.actual_loop_interval = self.config.G_SENSOR_INTERVAL - while not self.config.G_QUIT: - await asyncio.sleep(self.wait_time) - start_time = datetime.datetime.now() - # print(start_time, self.wait_time) - - time_profile = [ - start_time, - ] - hr = spd = cdc = pwr = temperature = self.config.G_ANT_NULLVALUE - grade = grade_spd = glide = self.config.G_ANT_NULLVALUE - ttlwork_diff = 0 - dst_diff = {"ANT+": 0, "GPS": 0, "USE": 0} - alt_diff = {"ANT+": 0, "GPS": 0, "USE": 0} - dst_diff_spd = {"ANT+": 0} - alt_diff_spd = {"ANT+": 0} - grade_use = {"ANT+": False, "GPS": False} - time_profile.append(datetime.datetime.now()) - # self.sensor_i2c.update() - # self.sensor_gps.update() - self.sensor_ant.update() # for dummy - - now_time = datetime.datetime.now() - time_profile.append(now_time) - - ant_id_type = self.config.G_ANT["ID_TYPE"] - delta = { - "PWR": {0x10: float("inf"), 0x11: float("inf"), 0x12: float("inf")} - } - for key in ["HR", "SPD", "CDC", "TEMP", "GPS"]: - delta[key] = float("inf") - # need for ANT+ ID update - for key in ["HR", "SPD", "CDC", "PWR", "TEMP"]: - if ( - self.config.G_ANT["USE"][key] - and ant_id_type[key] in self.values["ANT+"] - ): - v[key] = self.values["ANT+"][ant_id_type[key]] - - # make intervals from timestamp - for key in ["HR", "SPD", "CDC", "TEMP"]: - if not self.config.G_ANT["USE"][key]: - continue - if "timestamp" in v[key]: - delta[key] = (now_time - v[key]["timestamp"]).total_seconds() - # override: - # cadence from power - if self.config.G_ANT["TYPE"][key] == 0x0B and key == "CDC": - for page in [0x12, 0x10]: - if not "timestamp" in v[key][page]: + try: + while True: + await asyncio.sleep(self.wait_time) + start_time = datetime.datetime.now() + # print(start_time, self.wait_time) + + time_profile = [ + start_time, + ] + hr = spd = cdc = pwr = temperature = self.config.G_ANT_NULLVALUE + grade = grade_spd = glide = self.config.G_ANT_NULLVALUE + ttlwork_diff = 0 + dst_diff = {"ANT+": 0, "GPS": 0, "USE": 0} + alt_diff = {"ANT+": 0, "GPS": 0, "USE": 0} + dst_diff_spd = {"ANT+": 0} + alt_diff_spd = {"ANT+": 0} + grade_use = {"ANT+": False, "GPS": False} + time_profile.append(datetime.datetime.now()) + # self.sensor_i2c.update() + # self.sensor_gps.update() + self.sensor_ant.update() # for dummy + + now_time = datetime.datetime.now() + time_profile.append(now_time) + + ant_id_type = self.config.G_ANT["ID_TYPE"] + delta = { + "PWR": {0x10: float("inf"), 0x11: float("inf"), 0x12: float("inf")} + } + for key in ["HR", "SPD", "CDC", "TEMP", "GPS"]: + delta[key] = float("inf") + # need for ANT+ ID update + for key in ["HR", "SPD", "CDC", "PWR", "TEMP"]: + if ( + self.config.G_ANT["USE"][key] + and ant_id_type[key] in self.values["ANT+"] + ): + v[key] = self.values["ANT+"][ant_id_type[key]] + + # make intervals from timestamp + for key in ["HR", "SPD", "CDC", "TEMP"]: + if not self.config.G_ANT["USE"][key]: + continue + if "timestamp" in v[key]: + delta[key] = (now_time - v[key]["timestamp"]).total_seconds() + # override: + # cadence from power + if self.config.G_ANT["TYPE"][key] == 0x0B and key == "CDC": + for page in [0x12, 0x10]: + if not "timestamp" in v[key][page]: + continue + delta[key] = ( + now_time - v[key][page]["timestamp"] + ).total_seconds() + break + # speed from power + elif self.config.G_ANT["TYPE"][key] == 0x0B and key == "SPD": + if not "timestamp" in v[key][0x11]: continue delta[key] = ( - now_time - v[key][page]["timestamp"] + now_time - v[key][0x11]["timestamp"] ).total_seconds() - break - # speed from power - elif self.config.G_ANT["TYPE"][key] == 0x0B and key == "SPD": - if not "timestamp" in v[key][0x11]: - continue - delta[key] = (now_time - v[key][0x11]["timestamp"]).total_seconds() - # timestamp(power) - if self.config.G_ANT["USE"]["PWR"]: - for page in [0x12, 0x11, 0x10]: - if not "timestamp" in v["PWR"][page]: - continue - delta["PWR"][page] = ( - now_time - v["PWR"][page]["timestamp"] - ).total_seconds() - if "timestamp" in v["GPS"]: - delta["GPS"] = (now_time - v["GPS"]["timestamp"]).total_seconds() - - # HeartRate : ANT+ - if self.config.G_ANT["USE"]["HR"]: - if delta["HR"] < self.time_threshold["HR"]: - hr = v["HR"]["heart_rate"] - - # Cadence : ANT+ - if self.config.G_ANT["USE"]["CDC"]: - cdc = 0 - # get from cadence or speed&cadence sensor - if self.config.G_ANT["TYPE"]["CDC"] in [0x79, 0x7A]: - if delta["CDC"] < self.time_threshold["CDC"]: - cdc = v["CDC"]["cadence"] - # get from powermeter - elif self.config.G_ANT["TYPE"]["CDC"] == 0x0B: - for page in [0x12, 0x10]: - if not "timestamp" in v["CDC"][page]: + # timestamp(power) + if self.config.G_ANT["USE"]["PWR"]: + for page in [0x12, 0x11, 0x10]: + if not "timestamp" in v["PWR"][page]: continue + delta["PWR"][page] = ( + now_time - v["PWR"][page]["timestamp"] + ).total_seconds() + if "timestamp" in v["GPS"]: + delta["GPS"] = (now_time - v["GPS"]["timestamp"]).total_seconds() + + # HeartRate : ANT+ + if self.config.G_ANT["USE"]["HR"]: + if delta["HR"] < self.time_threshold["HR"]: + hr = v["HR"]["heart_rate"] + + # Cadence : ANT+ + if self.config.G_ANT["USE"]["CDC"]: + cdc = 0 + # get from cadence or speed&cadence sensor + if self.config.G_ANT["TYPE"]["CDC"] in [0x79, 0x7A]: if delta["CDC"] < self.time_threshold["CDC"]: - cdc = v["CDC"][page]["cadence"] + cdc = v["CDC"]["cadence"] + # get from powermeter + elif self.config.G_ANT["TYPE"]["CDC"] == 0x0B: + for page in [0x12, 0x10]: + if not "timestamp" in v["CDC"][page]: + continue + if delta["CDC"] < self.time_threshold["CDC"]: + cdc = v["CDC"][page]["cadence"] + break + + # Power : ANT+(assumed crank type > wheel type) + if self.config.G_ANT["USE"]["PWR"]: + pwr = 0 + # page18 > 17 > 16, 16simple is not used + for page in [0x12, 0x11, 0x10]: + if delta["PWR"][page] < self.time_threshold["PWR"]: + pwr = v["PWR"][page]["power"] break - # Power : ANT+(assumed crank type > wheel type) - if self.config.G_ANT["USE"]["PWR"]: - pwr = 0 - # page18 > 17 > 16, 16simple is not used - for page in [0x12, 0x11, 0x10]: - if delta["PWR"][page] < self.time_threshold["PWR"]: - pwr = v["PWR"][page]["power"] - break - - # Speed : ANT+(SPD&CDC, (PWR)) > GPS - if self.config.G_ANT["USE"]["SPD"]: - spd = 0 - if self.config.G_ANT["TYPE"]["SPD"] in [0x79, 0x7B]: - if delta["SPD"] < self.time_threshold["SPD"]: - spd = v["SPD"]["speed"] - elif self.config.G_ANT["TYPE"]["SPD"] == 0x0B: - if delta["SPD"] < self.time_threshold["SPD"]: - spd = v["SPD"][0x11]["speed"] - # complement from GPS speed when I2C acc sensor is available (using moving status) - if ( - delta["SPD"] > self.time_threshold["SPD"] - and spd == 0 - and v["I2C"]["m_stat"] == 1 - and v["GPS"]["speed"] > 0 - ): - spd = v["GPS"]["speed"] - # print("speed from GPS: delta {}s, {:.1f}km/h".format(delta['SPD'], v['GPS']['speed']*3.6)) - elif "timestamp" in v["GPS"]: - spd = 0 - if ( - not np.isnan(v["GPS"]["speed"]) - and delta["GPS"] < self.time_threshold["SPD"] - ): - spd = v["GPS"]["speed"] - - # Distance: ANT+(SPD, (PWR)) > GPS - if self.config.G_ANT["USE"]["SPD"]: - # normal speed meter - if self.config.G_ANT["TYPE"]["SPD"] in [0x79, 0x7B]: - if pre_dst["ANT+"] < v["SPD"]["distance"]: - dst_diff["ANT+"] = v["SPD"]["distance"] - pre_dst["ANT+"] - pre_dst["ANT+"] = v["SPD"]["distance"] - elif self.config.G_ANT["TYPE"]["SPD"] == 0x0B: - if pre_dst["ANT+"] < v["SPD"][0x11]["distance"]: - dst_diff["ANT+"] = v["SPD"][0x11]["distance"] - pre_dst["ANT+"] - pre_dst["ANT+"] = v["SPD"][0x11]["distance"] - dst_diff["USE"] = dst_diff["ANT+"] - grade_use["ANT+"] = True - if "timestamp" in v["GPS"]: - if pre_dst["GPS"] < v["GPS"]["distance"]: - dst_diff["GPS"] = v["GPS"]["distance"] - pre_dst["GPS"] - pre_dst["GPS"] = v["GPS"]["distance"] - if not self.config.G_ANT["USE"]["SPD"] and dst_diff["GPS"] > 0: - dst_diff["USE"] = dst_diff["GPS"] - grade_use["GPS"] = True - # ANT+ sensor is not connected from the beginning of the ride - elif self.config.G_ANT["USE"]["SPD"]: + # Speed : ANT+(SPD&CDC, (PWR)) > GPS + if self.config.G_ANT["USE"]["SPD"]: + spd = 0 + if self.config.G_ANT["TYPE"]["SPD"] in [0x79, 0x7B]: + if delta["SPD"] < self.time_threshold["SPD"]: + spd = v["SPD"]["speed"] + elif self.config.G_ANT["TYPE"]["SPD"] == 0x0B: + if delta["SPD"] < self.time_threshold["SPD"]: + spd = v["SPD"][0x11]["speed"] + # complement from GPS speed when I2C acc sensor is available (using moving status) if ( - delta["SPD"] == np.inf - and dst_diff["ANT+"] == 0 - and dst_diff["GPS"] > 0 + delta["SPD"] > self.time_threshold["SPD"] + and spd == 0 + and v["I2C"]["m_stat"] == 1 + and v["GPS"]["speed"] > 0 ): + spd = v["GPS"]["speed"] + # print("speed from GPS: delta {}s, {:.1f}km/h".format(delta['SPD'], v['GPS']['speed']*3.6)) + elif "timestamp" in v["GPS"]: + spd = 0 + if ( + not np.isnan(v["GPS"]["speed"]) + and delta["GPS"] < self.time_threshold["SPD"] + ): + spd = v["GPS"]["speed"] + + # Distance: ANT+(SPD, (PWR)) > GPS + if self.config.G_ANT["USE"]["SPD"]: + # normal speed meter + if self.config.G_ANT["TYPE"]["SPD"] in [0x79, 0x7B]: + if pre_dst["ANT+"] < v["SPD"]["distance"]: + dst_diff["ANT+"] = v["SPD"]["distance"] - pre_dst["ANT+"] + pre_dst["ANT+"] = v["SPD"]["distance"] + elif self.config.G_ANT["TYPE"]["SPD"] == 0x0B: + if pre_dst["ANT+"] < v["SPD"][0x11]["distance"]: + dst_diff["ANT+"] = ( + v["SPD"][0x11]["distance"] - pre_dst["ANT+"] + ) + pre_dst["ANT+"] = v["SPD"][0x11]["distance"] + dst_diff["USE"] = dst_diff["ANT+"] + grade_use["ANT+"] = True + if "timestamp" in v["GPS"]: + if pre_dst["GPS"] < v["GPS"]["distance"]: + dst_diff["GPS"] = v["GPS"]["distance"] - pre_dst["GPS"] + pre_dst["GPS"] = v["GPS"]["distance"] + if not self.config.G_ANT["USE"]["SPD"] and dst_diff["GPS"] > 0: dst_diff["USE"] = dst_diff["GPS"] - grade_use["ANT+"] = False grade_use["GPS"] = True + # ANT+ sensor is not connected from the beginning of the ride + elif self.config.G_ANT["USE"]["SPD"]: + if ( + delta["SPD"] == np.inf + and dst_diff["ANT+"] == 0 + and dst_diff["GPS"] > 0 + ): + dst_diff["USE"] = dst_diff["GPS"] + grade_use["ANT+"] = False + grade_use["GPS"] = True + + # Total Power: ANT+ + if self.config.G_ANT["USE"]["PWR"]: + # both type are not exist in same ID(0x12:crank, 0x11:wheel) + # if 0x12 or 0x11 exists, never take 0x10 + for page in [0x12, 0x11, 0x10]: + if "timestamp" in v["PWR"][page]: + if ( + pre_ttlwork["ANT+"] + < v["PWR"][page]["accumulated_power"] + ): + ttlwork_diff = ( + v["PWR"][page]["accumulated_power"] + - pre_ttlwork["ANT+"] + ) + pre_ttlwork["ANT+"] = v["PWR"][page]["accumulated_power"] + # never take other powermeter + break - # Total Power: ANT+ - if self.config.G_ANT["USE"]["PWR"]: - # both type are not exist in same ID(0x12:crank, 0x11:wheel) - # if 0x12 or 0x11 exists, never take 0x10 - for page in [0x12, 0x11, 0x10]: - if "timestamp" in v["PWR"][page]: - if pre_ttlwork["ANT+"] < v["PWR"][page]["accumulated_power"]: - ttlwork_diff = ( - v["PWR"][page]["accumulated_power"] - - pre_ttlwork["ANT+"] - ) - pre_ttlwork["ANT+"] = v["PWR"][page]["accumulated_power"] - # never take other powermeter - break - - # Temperature : ANT+ - if self.config.G_ANT["USE"]["TEMP"]: - if delta["TEMP"] < self.time_threshold["TEMP"]: - temperature = v["TEMP"]["temperature"] - elif not np.isnan(v["I2C"]["temperature"]): - temperature = v["I2C"]["temperature"] - - # altitude - if not np.isnan(v["I2C"]["pre_altitude"]): - alt = v["I2C"]["altitude"] - # for grade (distance base) - for key in ["ANT+", "GPS"]: - if dst_diff[key] > 0: - alt_diff[key] = alt - pre_alt[key] - pre_alt[key] = alt - if self.config.G_ANT["USE"]["SPD"]: - alt_diff["USE"] = alt_diff["ANT+"] - elif not self.config.G_ANT["USE"]["SPD"] and dst_diff["GPS"] > 0: - alt_diff["USE"] = alt_diff["GPS"] - # for grade (speed base) - if self.config.G_ANT["USE"]["SPD"]: - if dst_diff["ANT+"] > 0: - alt_diff_spd["ANT+"] = alt - pre_alt_spd["ANT+"] - pre_alt_spd["ANT+"] = alt - # dem_altitude - if self.config.G_LOG_ALTITUDE_FROM_DATA_SOURCE: - self.values["integrated"][ - "dem_altitude" - ] = await self.config.get_altitude_from_tile( - [v["GPS"]["lon"], v["GPS"]["lat"]] - ) - - # grade (distance base) - if dst_diff["USE"] > 0: - for key in ["alt_diff", "dst_diff"]: - self.values["integrated"][key][0:-1] = self.values["integrated"][ - key - ][1:] - self.values["integrated"][key][-1] = eval(key + "['USE']") - # diff_sum[key] = np.mean(self.values['integrated'][key][-self.grade_window_size:]) - diff_sum[key] = np.nansum( - self.values["integrated"][key][-self.grade_window_size :] - ) - # set grade - gl = self.config.G_ANT_NULLVALUE - gr = self.config.G_ANT_NULLVALUE - x = self.config.G_ANT_NULLVALUE - y = diff_sum["alt_diff"] - if grade_use["ANT+"]: - x = math.sqrt( - abs(diff_sum["dst_diff"] ** 2 - diff_sum["alt_diff"] ** 2) - ) - elif grade_use["GPS"]: - x = diff_sum["dst_diff"] - if x > 0: - # gr = int(round(100 * y / x)) - gr = self.conv_grade(100 * y / x) - if y != 0.0: - gl = int(round(-1 * x / y)) - grade = pre_grade = gr - glide = pre_glide = gl - # for sometimes ANT+ distance is 0 although status is running - elif dst_diff["USE"] == 0 and self.config.G_STOPWATCH_STATUS == "START": - grade = pre_grade - glide = pre_glide - - # grade (speed base) - if self.config.G_ANT["USE"]["SPD"]: - dst_diff_spd["ANT+"] = spd * self.actual_loop_interval - for key in ["alt_diff_spd", "dst_diff_spd"]: - self.values["integrated"][key][0:-1] = self.values["integrated"][ - key - ][1:] - self.values["integrated"][key][-1] = eval(key + "['ANT+']") - diff_sum[key] = np.mean( - self.values["integrated"][key][-self.grade_window_size :] + # Temperature : ANT+ + if self.config.G_ANT["USE"]["TEMP"]: + if delta["TEMP"] < self.time_threshold["TEMP"]: + temperature = v["TEMP"]["temperature"] + elif not np.isnan(v["I2C"]["temperature"]): + temperature = v["I2C"]["temperature"] + + # altitude + if not np.isnan(v["I2C"]["pre_altitude"]): + alt = v["I2C"]["altitude"] + # for grade (distance base) + for key in ["ANT+", "GPS"]: + if dst_diff[key] > 0: + alt_diff[key] = alt - pre_alt[key] + pre_alt[key] = alt + if self.config.G_ANT["USE"]["SPD"]: + alt_diff["USE"] = alt_diff["ANT+"] + elif not self.config.G_ANT["USE"]["SPD"] and dst_diff["GPS"] > 0: + alt_diff["USE"] = alt_diff["GPS"] + # for grade (speed base) + if self.config.G_ANT["USE"]["SPD"]: + if dst_diff["ANT+"] > 0: + alt_diff_spd["ANT+"] = alt - pre_alt_spd["ANT+"] + pre_alt_spd["ANT+"] = alt + # dem_altitude + if self.config.G_LOG_ALTITUDE_FROM_DATA_SOURCE: + self.values["integrated"][ + "dem_altitude" + ] = await self.config.get_altitude_from_tile( + [v["GPS"]["lon"], v["GPS"]["lat"]] ) - # diff_sum[key] = np.nansum(self.values['integrated'][key][-self.grade_window_size:]) - # set grade - x = diff_sum["dst_diff_spd"] ** 2 - diff_sum["alt_diff_spd"] ** 2 - y = diff_sum["alt_diff_spd"] - gr = self.config.G_ANT_NULLVALUE - if x > 0: - x = math.sqrt(x) - gr = self.conv_grade(100 * y / x) - grade_spd = pre_grade_spd = gr - # for sometimes speed sensor value is missing in running - elif ( - dst_diff_spd["ANT+"] == 0 and self.config.G_STOPWATCH_STATUS == "START" - ): - grade_spd = pre_grade_spd - - self.values["integrated"]["heart_rate"] = hr - self.values["integrated"]["speed"] = spd - self.values["integrated"]["cadence"] = cdc - self.values["integrated"]["power"] = pwr - self.values["integrated"]["distance"] += dst_diff["USE"] - self.values["integrated"]["accumulated_power"] += ttlwork_diff - self.values["integrated"]["grade"] = grade - self.values["integrated"]["grade_spd"] = grade_spd - self.values["integrated"]["glide_ratio"] = glide - self.values["integrated"]["temperature"] = temperature - - for g in self.graph_keys: - self.values["integrated"][g][0:-1] = self.values["integrated"][g][1:] - self.values["integrated"]["hr_graph"][-1] = hr - self.values["integrated"]["power_graph"][-1] = pwr - self.values["integrated"]["w_bal_graph"][-1] = self.values["integrated"][ - "w_prime_balance_normalized" - ] - self.values["integrated"]["altitude_gps_graph"][-1] = v["GPS"]["alt"] - self.values["integrated"]["altitude_graph"][-1] = v["I2C"]["altitude"] - - # average power, heart_rate - if self.config.G_ANT["USE"]["PWR"] and not np.isnan(pwr): - self.get_ave_values("power", pwr) - if self.config.G_ANT["USE"]["HR"] and not np.isnan(hr): - self.get_ave_values("heart_rate", hr) - - if self.config.G_ANT["USE"]["PWR"]: - # w_prime_balance - # https://medium.com/critical-powers/comparison-of-wbalance-algorithms-8838173e2c15 - - # Waterworth algorighm - if self.config.G_POWER_W_PRIME_ALGORITHM == "WATERWORTH": - pwr_cp_diff = pwr - self.config.G_POWER_CP - if self.config.G_POWER_CP < 0: - self.values["integrated"]["w_prime_power_sum"] = ( - self.values["integrated"]["w_prime_power_sum"] + pwr + + # grade (distance base) + if dst_diff["USE"] > 0: + for key in ["alt_diff", "dst_diff"]: + self.values["integrated"][key][0:-1] = self.values[ + "integrated" + ][key][1:] + self.values["integrated"][key][-1] = eval(key + "['USE']") + # diff_sum[key] = np.mean(self.values['integrated'][key][-self.grade_window_size:]) + diff_sum[key] = np.nansum( + self.values["integrated"][key][-self.grade_window_size :] ) - self.values["integrated"]["w_prime_power_count"] = ( - self.values["integrated"]["w_prime_power_count"] + 1 + # set grade + gl = self.config.G_ANT_NULLVALUE + gr = self.config.G_ANT_NULLVALUE + x = self.config.G_ANT_NULLVALUE + y = diff_sum["alt_diff"] + if grade_use["ANT+"]: + x = math.sqrt( + abs(diff_sum["dst_diff"] ** 2 - diff_sum["alt_diff"] ** 2) ) - pwr_mean_under_cp = ( - self.values["integrated"]["w_prime_power_sum"] - / self.values["integrated"]["w_prime_power_count"] + elif grade_use["GPS"]: + x = diff_sum["dst_diff"] + if x > 0: + # gr = int(round(100 * y / x)) + gr = self.conv_grade(100 * y / x) + if y != 0.0: + gl = int(round(-1 * x / y)) + grade = pre_grade = gr + glide = pre_glide = gl + # for sometimes ANT+ distance is 0 although status is running + elif dst_diff["USE"] == 0 and self.config.G_STOPWATCH_STATUS == "START": + grade = pre_grade + glide = pre_glide + + # grade (speed base) + if self.config.G_ANT["USE"]["SPD"]: + dst_diff_spd["ANT+"] = spd * self.actual_loop_interval + for key in ["alt_diff_spd", "dst_diff_spd"]: + self.values["integrated"][key][0:-1] = self.values[ + "integrated" + ][key][1:] + self.values["integrated"][key][-1] = eval(key + "['ANT+']") + diff_sum[key] = np.mean( + self.values["integrated"][key][-self.grade_window_size :] ) - tau = ( - 546 - * np.exp( - -0.01 * (self.config.G_POWER_CP - pwr_mean_under_cp) + # diff_sum[key] = np.nansum(self.values['integrated'][key][-self.grade_window_size:]) + # set grade + x = diff_sum["dst_diff_spd"] ** 2 - diff_sum["alt_diff_spd"] ** 2 + y = diff_sum["alt_diff_spd"] + gr = self.config.G_ANT_NULLVALUE + if x > 0: + x = math.sqrt(x) + gr = self.conv_grade(100 * y / x) + grade_spd = pre_grade_spd = gr + # for sometimes speed sensor value is missing in running + elif ( + dst_diff_spd["ANT+"] == 0 + and self.config.G_STOPWATCH_STATUS == "START" + ): + grade_spd = pre_grade_spd + + self.values["integrated"]["heart_rate"] = hr + self.values["integrated"]["speed"] = spd + self.values["integrated"]["cadence"] = cdc + self.values["integrated"]["power"] = pwr + self.values["integrated"]["distance"] += dst_diff["USE"] + self.values["integrated"]["accumulated_power"] += ttlwork_diff + self.values["integrated"]["grade"] = grade + self.values["integrated"]["grade_spd"] = grade_spd + self.values["integrated"]["glide_ratio"] = glide + self.values["integrated"]["temperature"] = temperature + + for g in self.graph_keys: + self.values["integrated"][g][0:-1] = self.values["integrated"][g][ + 1: + ] + self.values["integrated"]["hr_graph"][-1] = hr + self.values["integrated"]["power_graph"][-1] = pwr + self.values["integrated"]["w_bal_graph"][-1] = self.values[ + "integrated" + ]["w_prime_balance_normalized"] + self.values["integrated"]["altitude_gps_graph"][-1] = v["GPS"]["alt"] + self.values["integrated"]["altitude_graph"][-1] = v["I2C"]["altitude"] + + # average power, heart_rate + if self.config.G_ANT["USE"]["PWR"] and not np.isnan(pwr): + self.get_ave_values("power", pwr) + if self.config.G_ANT["USE"]["HR"] and not np.isnan(hr): + self.get_ave_values("heart_rate", hr) + + if self.config.G_ANT["USE"]["PWR"]: + # w_prime_balance + # https://medium.com/critical-powers/comparison-of-wbalance-algorithms-8838173e2c15 + + # Waterworth algorighm + if self.config.G_POWER_W_PRIME_ALGORITHM == "WATERWORTH": + pwr_cp_diff = pwr - self.config.G_POWER_CP + if self.config.G_POWER_CP < 0: + self.values["integrated"]["w_prime_power_sum"] = ( + self.values["integrated"]["w_prime_power_sum"] + pwr + ) + self.values["integrated"]["w_prime_power_count"] = ( + self.values["integrated"]["w_prime_power_count"] + 1 + ) + pwr_mean_under_cp = ( + self.values["integrated"]["w_prime_power_sum"] + / self.values["integrated"]["w_prime_power_count"] ) - + 316 + tau = ( + 546 + * np.exp( + -0.01 * (self.config.G_POWER_CP - pwr_mean_under_cp) + ) + + 316 + ) + self.values["integrated"]["w_prime_sum"] = self.values[ + "integrated" + ]["w_prime_sum"] + max(0, pwr_cp_diff) * np.exp( + self.values["integrated"]["w_prime_t"] / tau ) - self.values["integrated"]["w_prime_sum"] = self.values[ - "integrated" - ]["w_prime_sum"] + max(0, pwr_cp_diff) * np.exp( - self.values["integrated"]["w_prime_t"] / tau - ) - self.values["integrated"][ - "w_prime_balance" - ] = self.config.G_POWER_W_PRIME - self.values["integrated"][ - "w_prime_sum" - ] * np.exp( - -1 * self.values["integrated"]["w_prime_t"] / tau - ) - self.values["integrated"]["w_prime_t"] = ( - self.values["integrated"]["w_prime_t"] - + self.config.G_SENSOR_INTERVAL - ) - - # Differential algorithm - elif self.config.G_POWER_W_PRIME_ALGORITHM == "DIFFERENTIAL": - cp_pwr_diff = self.config.G_POWER_CP - pwr - w_bal_t = self.values["integrated"]["w_prime_balance"] - if cp_pwr_diff < 0: - # consume - self.values["integrated"]["w_prime_balance"] = ( - w_bal_t + cp_pwr_diff + self.values["integrated"][ + "w_prime_balance" + ] = self.config.G_POWER_W_PRIME - self.values["integrated"][ + "w_prime_sum" + ] * np.exp( + -1 * self.values["integrated"]["w_prime_t"] / tau ) - else: - # recovery - self.values["integrated"]["w_prime_balance"] = ( - w_bal_t - + cp_pwr_diff - * (self.config.G_POWER_W_PRIME - w_bal_t) - / self.config.G_POWER_W_PRIME + self.values["integrated"]["w_prime_t"] = ( + self.values["integrated"]["w_prime_t"] + + self.config.G_SENSOR_INTERVAL ) - self.values["integrated"]["w_prime_balance_normalized"] = int( - self.values["integrated"]["w_prime_balance"] - / self.config.G_POWER_W_PRIME - * 100 - ) - - time_profile.append(datetime.datetime.now()) - - # toggle auto stop - # ANT+ or GPS speed is available - if not np.isnan(spd) and self.config.G_MANUAL_STATUS == "START": - # speed from ANT+ or GPS - flag_spd = False - if spd >= self.config.G_AUTOSTOP_CUTOFF: - flag_spd = True - - # use moving status of accelerometer because of excluding erroneous speed values when stopping - flag_moving = False - if v["I2C"]["m_stat"] == 1: - flag_moving = True - - # flag_moving is not considered (set True) as follows, - # accelerometer is not available (nan) - # ANT+ speed sensor is available - if ( - np.isnan(v["I2C"]["m_stat"]) - or self.config.G_ANT["USE"]["SPD"] - or self.config.G_DUMMY_OUTPUT - ): - flag_moving = True + # Differential algorithm + elif self.config.G_POWER_W_PRIME_ALGORITHM == "DIFFERENTIAL": + cp_pwr_diff = self.config.G_POWER_CP - pwr + w_bal_t = self.values["integrated"]["w_prime_balance"] + if cp_pwr_diff < 0: + # consume + self.values["integrated"]["w_prime_balance"] = ( + w_bal_t + cp_pwr_diff + ) + else: + # recovery + self.values["integrated"]["w_prime_balance"] = ( + w_bal_t + + cp_pwr_diff + * (self.config.G_POWER_W_PRIME - w_bal_t) + / self.config.G_POWER_W_PRIME + ) - if ( - self.config.G_STOPWATCH_STATUS == "STOP" - and flag_spd - and flag_moving - and self.config.logger is not None - ): - self.config.logger.start_and_stop() - elif ( - self.config.G_STOPWATCH_STATUS == "START" - and (not flag_spd or not flag_moving) - and self.config.logger is not None - ): - self.config.logger.start_and_stop() + self.values["integrated"]["w_prime_balance_normalized"] = int( + self.values["integrated"]["w_prime_balance"] + / self.config.G_POWER_W_PRIME + * 100 + ) - # ANT+ or GPS speed is not available - elif np.isnan(spd) and self.config.G_MANUAL_STATUS == "START": - # stop recording if speed is broken - if ( - (self.config.G_ANT["USE"]["SPD"] or "timestamp" in v["GPS"]) - and self.config.G_STOPWATCH_STATUS == "START" - and self.config.logger is not None - ): - self.config.logger.start_and_stop() - # time.sleep(1) - - # auto backlight - if self.config.G_IS_RASPI and self.config.G_USE_AUTO_BACKLIGHT: - if ( - self.config.G_DISPLAY in ("MIP", "MIP_640") - and self.config.display.send_display - and not np.isnan(v["I2C"]["light"]) - ): - if v["I2C"]["light"] <= self.config.G_AUTO_BACKLIGHT_CUTOFF: - self.config.display.display.set_brightness(3) - else: - self.config.display.display.set_brightness(0) - if self.config.G_MANUAL_STATUS == "START": - if v["I2C"]["light"] <= self.config.G_AUTO_BACKLIGHT_CUTOFF: - self.sensor_ant.set_light_mode("FLASH_LOW", auto=True) - else: - self.sensor_ant.set_light_mode("OFF", auto=True) - - # cpu and memory - if _IMPORT_PSUTIL: - self.values["integrated"]["cpu_percent"] = int( - self.process.cpu_percent(interval=None) - ) - self.values["integrated"][ - "CPU_MEM" - ] = "{0:^2.0f}% ({1}) / ALL {2:^2.0f}%, {3:^2.0f}%".format( + time_profile.append(datetime.datetime.now()) + + # toggle auto stop + # ANT+ or GPS speed is available + if not np.isnan(spd) and self.config.G_MANUAL_STATUS == "START": + # speed from ANT+ or GPS + flag_spd = False + if spd >= self.config.G_AUTOSTOP_CUTOFF: + flag_spd = True + + # use moving status of accelerometer because of excluding erroneous speed values when stopping + flag_moving = False + if v["I2C"]["m_stat"] == 1: + flag_moving = True + + # flag_moving is not considered (set True) as follows, + # accelerometer is not available (nan) + # ANT+ speed sensor is available + if ( + np.isnan(v["I2C"]["m_stat"]) + or self.config.G_ANT["USE"]["SPD"] + or self.config.G_DUMMY_OUTPUT + ): + flag_moving = True + + if ( + self.config.G_STOPWATCH_STATUS == "STOP" + and flag_spd + and flag_moving + and self.config.logger is not None + ): + self.config.logger.start_and_stop() + elif ( + self.config.G_STOPWATCH_STATUS == "START" + and (not flag_spd or not flag_moving) + and self.config.logger is not None + ): + self.config.logger.start_and_stop() + + # ANT+ or GPS speed is not available + elif np.isnan(spd) and self.config.G_MANUAL_STATUS == "START": + # stop recording if speed is broken + if ( + (self.config.G_ANT["USE"]["SPD"] or "timestamp" in v["GPS"]) + and self.config.G_STOPWATCH_STATUS == "START" + and self.config.logger is not None + ): + self.config.logger.start_and_stop() + # time.sleep(1) + + # auto backlight + if self.config.G_IS_RASPI and self.config.G_USE_AUTO_BACKLIGHT: + if ( + self.config.G_DISPLAY in ("MIP", "MIP_640") + and self.config.display.send_display + and not np.isnan(v["I2C"]["light"]) + ): + if v["I2C"]["light"] <= self.config.G_AUTO_BACKLIGHT_CUTOFF: + self.config.display.display.set_brightness(3) + else: + self.config.display.display.set_brightness(0) + if self.config.G_MANUAL_STATUS == "START": + if v["I2C"]["light"] <= self.config.G_AUTO_BACKLIGHT_CUTOFF: + self.sensor_ant.set_light_mode("FLASH_LOW", auto=True) + else: + self.sensor_ant.set_light_mode("OFF", auto=True) + + # cpu and memory + if _IMPORT_PSUTIL: + self.values["integrated"]["cpu_percent"] = int( + self.process.cpu_percent(interval=None) + ) self.values["integrated"][ - "cpu_percent" - ], # self.process.cpu_percent(interval=None), - self.process.num_threads(), - psutil.cpu_percent(interval=None), - self.process.memory_percent(), - ) - - # adjust loop time - time_profile.append(datetime.datetime.now()) - sec_diff = [] - time_progile_sec = 0 - for i in range(len(time_profile)): - if i == 0: - continue - sec_diff.append( - "{0:.6f}".format( - (time_profile[i] - time_profile[i - 1]).total_seconds() + "CPU_MEM" + ] = "{0:^2.0f}% ({1}) / ALL {2:^2.0f}%, {3:^2.0f}%".format( + self.values["integrated"][ + "cpu_percent" + ], # self.process.cpu_percent(interval=None), + self.process.num_threads(), + psutil.cpu_percent(interval=None), + self.process.memory_percent(), + ) + + # adjust loop time + time_profile.append(datetime.datetime.now()) + sec_diff = [] + time_progile_sec = 0 + for i in range(len(time_profile)): + if i == 0: + continue + sec_diff.append( + "{0:.6f}".format( + (time_profile[i] - time_profile[i - 1]).total_seconds() + ) + ) + time_progile_sec += ( + time_profile[i] - time_profile[i - 1] + ).total_seconds() + if time_progile_sec > 1.5 * self.config.G_SENSOR_INTERVAL: + app_logger.warning( + f"too long loop time: {datetime.datetime.now().strftime('%Y%m%d %H:%M:%S')}, sec_diff: {sec_diff}" + ) + + loop_time = (datetime.datetime.now() - start_time).total_seconds() + d1, d2 = divmod(loop_time, self.config.G_SENSOR_INTERVAL) + if d1 > self.config.G_SENSOR_INTERVAL * 10: # [s] + app_logger.warning( + f"too long loop_time({self.__class__.__name__}):{loop_time:.2f}, interval:{self.config.G_SENSOR_INTERVAL:.1f}" ) - ) - time_progile_sec += ( - time_profile[i] - time_profile[i - 1] - ).total_seconds() - if time_progile_sec > 1.5 * self.config.G_SENSOR_INTERVAL: - app_logger.warning( - f"too long loop time: {datetime.datetime.now().strftime('%Y%m%d %H:%M:%S')}, sec_diff: {sec_diff}" - ) - - loop_time = (datetime.datetime.now() - start_time).total_seconds() - d1, d2 = divmod(loop_time, self.config.G_SENSOR_INTERVAL) - if d1 > self.config.G_SENSOR_INTERVAL * 10: # [s] - app_logger.warning( - f"too long loop_time({self.__class__.__name__}):{loop_time:.2f}, interval:{self.config.G_SENSOR_INTERVAL:.1f}" - ) - d1 = d2 = 0 - self.wait_time = self.config.G_SENSOR_INTERVAL - d2 - self.actual_loop_interval = (d1 + 1) * self.config.G_SENSOR_INTERVAL + d1 = d2 = 0 + self.wait_time = self.config.G_SENSOR_INTERVAL - d2 + self.actual_loop_interval = (d1 + 1) * self.config.G_SENSOR_INTERVAL + except asyncio.CancelledError: + pass @staticmethod def conv_grade(gr): diff --git a/modules/utils/filters/__init__.py b/modules/utils/filters/__init__.py new file mode 100644 index 00000000..302decc5 --- /dev/null +++ b/modules/utils/filters/__init__.py @@ -0,0 +1,2 @@ +from .kalman import KalmanFilter, KalmanFilter_pitch # noqa +from .savitzky_golay import savitzky_golay # noqa diff --git a/modules/sensor/kalman_filter.py b/modules/utils/filters/kalman.py similarity index 99% rename from modules/sensor/kalman_filter.py rename to modules/utils/filters/kalman.py index 6c1e580c..462d1cf0 100644 --- a/modules/sensor/kalman_filter.py +++ b/modules/utils/filters/kalman.py @@ -1,9 +1,9 @@ -# simplify filterpy +# simplify filterpy/kalman/kalman_filter -import sys -from math import log from copy import deepcopy - +from math import log +import sys +import numpy as np from numpy import dot, zeros, eye, isscalar, linalg, array, atleast_2d @@ -13,7 +13,7 @@ def reshape_z(z, dim_z, ndim): if z.shape[1] == dim_z: z = z.T if z.shape != (dim_z, 1): - raise ValueError("z must be convertible to shape ({}, 1)".format(dim_z)) + raise ValueError(f"z must be convertible to shape ({dim_z}, 1)") if ndim == 1: z = z[:, 0] if ndim == 0: @@ -364,8 +364,6 @@ def update(self, z, R=None, H=None): class KalmanFilter_pitch: - import numpy as np - theta_variance = 0 theta_dot_variance = 0 theta_update_interval = 0.1 diff --git a/modules/utils/filters/savitzky_golay.py b/modules/utils/filters/savitzky_golay.py new file mode 100644 index 00000000..cb9c0f47 --- /dev/null +++ b/modules/utils/filters/savitzky_golay.py @@ -0,0 +1,28 @@ +from math import factorial + +import numpy as np + + +def savitzky_golay(y, window_size, order, deriv=0, rate=1): + try: + window_size = np.abs(np.intc(window_size)) + order = np.abs(np.intc(order)) + except ValueError: + raise ValueError("window_size and order have to be of type int") + if window_size % 2 != 1 or window_size < 1: + raise TypeError("window_size size must be a positive odd number") + if window_size < order + 2: + raise TypeError("window_size is too small for the polynomials order") + order_range = range(order + 1) + half_window = (window_size - 1) // 2 + # precompute coefficients + b = np.mat( + [[k**i for i in order_range] for k in range(-half_window, half_window + 1)] + ) + m = np.linalg.pinv(b).A[deriv] * rate**deriv * factorial(deriv) + # pad the signal at the extremes with + # values taken from the signal itself + first_vals = y[0] - np.abs(y[1 : half_window + 1][::-1] - y[0]) + last_vals = y[-1] + np.abs(y[-half_window - 1 : -1][::-1] - y[-1]) + y = np.concatenate((first_vals, y, last_vals)) + return np.convolve(m[::-1], y, mode="valid") diff --git a/modules/utils/geo.py b/modules/utils/geo.py new file mode 100644 index 00000000..bdb36390 --- /dev/null +++ b/modules/utils/geo.py @@ -0,0 +1,127 @@ +import math +import traceback + +import numpy as np + +# TODO use Decimal not floats + +# for get_dist_on_earth +GEO_R1 = 6378.137 +GEO_R2 = 6356.752314140 +GEO_R1_2 = (GEO_R1 * 1000) ** 2 +GEO_R2_2 = (GEO_R2 * 1000) ** 2 +GEO_E2 = (GEO_R1_2 - GEO_R2_2) / GEO_R1_2 +G_DISTANCE_BY_LAT1S = GEO_R2 * 1000 * 2 * np.pi / 360 / 60 / 60 # [m] + +# for track +TRACK_STR = [ + "N", + "NE", + "E", + "SE", + "S", + "SW", + "W", + "NW", + "N", +] + + +def calc_azimuth(lat, lon): + rad_latitude = np.radians(lat) + rad_longitude = np.radians(lon) + rad_longitude_delta = rad_longitude[1:] - rad_longitude[0:-1] + azimuth = np.mod( + np.degrees( + np.arctan2( + np.sin(rad_longitude_delta), + np.cos(rad_latitude[0:-1]) * np.tan(rad_latitude[1:]) + - np.sin(rad_latitude[0:-1]) * np.cos(rad_longitude_delta), + ) + ), + 360, + ).astype(dtype="int16") + return azimuth + + +def calc_y_mod(lat): + if np.isnan(lat): + return np.nan + return GEO_R2 / (GEO_R1 * math.cos(lat / 180 * np.pi)) + + +# return [m] +def get_dist_on_earth(p0_lon, p0_lat, p1_lon, p1_lat): + if p0_lon == p1_lon and p0_lat == p1_lat: + return 0 + (r0_lon, r0_lat, r1_lon, r1_lat) = map( + math.radians, [p0_lon, p0_lat, p1_lon, p1_lat] + ) + delta_x = r1_lon - r0_lon + cos_d = math.sin(r0_lat) * math.sin(r1_lat) + math.cos(r0_lat) * math.cos( + r1_lat + ) * math.cos(delta_x) + try: + res = 1000 * math.acos(cos_d) * GEO_R1 + return res + except: + # traceback.print_exc() + # print("cos_d =", cos_d) + # print("parameter:", p0_lon, p0_lat, p1_lon, p1_lat) + return 0 + + +# return [m] +def get_dist_on_earth_array(p0_lon, p0_lat, p1_lon, p1_lat): + # if p0_lon == p1_lon and p0_lat == p1_lat: + # return 0 + r0_lon = np.radians(p0_lon) + r0_lat = np.radians(p0_lat) + r1_lon = np.radians(p1_lon) + r1_lat = np.radians(p1_lat) + # (r0_lon, r0_lat, r1_lon, r1_lat) = map(radians, [p0_lon, p0_lat, p1_lon, p1_lat]) + delta_x = r1_lon - r0_lon + cos_d = np.sin(r0_lat) * np.sin(r1_lat) + np.cos(r0_lat) * np.cos(r1_lat) * np.cos( + delta_x + ) + try: + res = 1000 * np.arccos(cos_d) * GEO_R1 + return res + except: + traceback.print_exc() + # #print("cos_d =", cos_d) + # #print("parameter:", p0_lon, p0_lat, p1_lon, p1_lat) + return np.array([]) + + +# return [m] +def get_dist_on_earth_hubeny(p0_lon, p0_lat, p1_lon, p1_lat): + if p0_lon == p1_lon and p0_lat == p1_lat: + return 0 + (r0_lon, r0_lat, r1_lon, r1_lat) = map( + math.radians, [p0_lon, p0_lat, p1_lon, p1_lat] + ) + lat_t = (r0_lat + r1_lat) / 2 + w = 1 - GEO_E2 * math.sin(lat_t) ** 2 + c2 = math.cos(lat_t) ** 2 + return math.sqrt( + (GEO_R2_2 / w**3) * (r0_lat - r1_lat) ** 2 + + (GEO_R1_2 / w) * c2 * (r0_lon - r1_lon) ** 2 + ) + + +def get_mod_lat(lat): + return lat * calc_y_mod(lat) + + +def get_mod_lat_np(lat): + return lat * GEO_R2 / (GEO_R1 * np.cos(lat / 180 * np.pi)) + + +def get_track_str(drc): + track_int = int((drc + 22.5) / 45.0) + return TRACK_STR[track_int] + + +def get_width_distance(lat, w): + return w * GEO_R1 * 1000 * 2 * np.pi * math.cos(lat / 180 * np.pi) / 360 diff --git a/modules/utils/map.py b/modules/utils/map.py new file mode 100644 index 00000000..8aac4698 --- /dev/null +++ b/modules/utils/map.py @@ -0,0 +1,40 @@ +import math +import os +import shutil + + +def get_maptile_filename(map_name, z, x, y): + return f"maptile/{map_name}/{z}/{x}/{y}.png" + + +def get_lon_lat_from_tile_xy(z, x, y): + n = 2.0**z + lon = x / n * 360.0 - 180.0 + lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n)))) + + return lon, lat + + +def get_tilexy_and_xy_in_tile(z, x, y, tile_size): + n = 2.0**z + _y = math.radians(y) + x_in_tile, tile_x = math.modf((x + 180.0) / 360.0 * n) + y_in_tile, tile_y = math.modf( + (1.0 - math.log(math.tan(_y) + (1.0 / math.cos(_y))) / math.pi) / 2.0 * n + ) + + return ( + int(tile_x), + int(tile_y), + int(x_in_tile * tile_size), + int(y_in_tile * tile_size), + ) + + +def remove_maptiles(map_name): + path = os.path.join("maptile", map_name) + if os.path.exists(path): + files = os.listdir(path) + dirs = [f for f in files if os.path.isdir(os.path.join(path, f))] + for d in dirs: + shutil.rmtree(os.path.join(path, d)) diff --git a/reqs/full.txt b/reqs/full.txt index 7aaeb397..7802fdfa 100644 --- a/reqs/full.txt +++ b/reqs/full.txt @@ -4,7 +4,7 @@ # # pip-compile --no-annotate --output-file=reqs/full.txt reqs/full.in # -aiofiles==23.2.0 +aiofiles==23.2.1 aiohttp==3.8.5 aiosignal==1.3.1 async-timeout==4.0.3 diff --git a/reqs/min.in b/reqs/min.in index c7f10106..d3cf2a89 100644 --- a/reqs/min.in +++ b/reqs/min.in @@ -1,5 +1,5 @@ aiohttp==3.8.5 -aiofiles==23.2. +aiofiles==23.2.1 git+https://github.com/hishizuka/crdp.git numpy==1.25.2 oyaml==1.0 diff --git a/reqs/min.txt b/reqs/min.txt index 39a33cfa..f75fb6c8 100644 --- a/reqs/min.txt +++ b/reqs/min.txt @@ -4,7 +4,7 @@ # # pip-compile --no-annotate --output-file=reqs/min.txt reqs/min.in # -aiofiles==23.2.0 +aiofiles==23.2.1 aiohttp==3.8.5 aiosignal==1.3.1 async-timeout==4.0.3