diff --git a/Drone/animation_lib.py b/Drone/animation_lib.py index 2018fdb..f96fd46 100644 --- a/Drone/animation_lib.py +++ b/Drone/animation_lib.py @@ -1,10 +1,12 @@ -import time +import os import csv import copy +import math +import time +import numpy import rospy import logging import threading -import ConfigParser try: from FlightLib import FlightLib @@ -15,198 +17,350 @@ try: except ImportError: print("Can't import LedLib") -import tasking_lib as tasking - logger = logging.getLogger(__name__) interrupt_event = threading.Event() -anim_id = "Empty id" +def moving(f1, f2, delta, x=True, y=True, z=True): + return ((abs(f1.x - f2.x) > delta) and x + or (abs(f1.y - f2.y) > delta) and y + or (abs(f1.z - f2.z) > delta) and z) -# TODO refactor as class -# TODO separate code for frames transformations (e.g. for gps) +def get_numbers(frames): + numbers = [] + if frames: + for frame in frames: + numbers.append(frame.number) + return numbers -def get_id(filepath="animation.csv"): - global anim_id - try: - animation_file = open(filepath) - except IOError: - logger.debug("File {} can't be opened".format(filepath)) - anim_id = "No animation" - return anim_id - else: - with animation_file: - csv_reader = csv.reader( - animation_file, delimiter=',', quotechar='|' - ) - row_0 = csv_reader.next() - if len(row_0) == 1: - anim_id = row_0[0] - logger.debug("Got animation_id: {}".format(anim_id)) - else: - anim_id = "Empty id" - logger.debug("No animation id in file") - return anim_id +class Frame(object): + params_dict = { + "number": None, + "x": None, + "y": None, + "z": None, + "yaw": None, + "red": None, + "green": None, + "blue": None, + "delay": None, + } + def __init__(self, csv_row=None, delay=None): + for key, value in self.params_dict.items(): + setattr(self, key, value) + if csv_row: + self.load_csv_row(csv_row) + if delay: + self.delay = delay -def get_start_xy(filepath="animation.csv", x_ratio=1, y_ratio=1, z_ratio=1): - try: - animation_file = open(filepath) - except IOError: - logger.debug("File {} can't be opened".format(filepath)) - anim_id = "No animation" - return float('nan'), float('nan') - else: - with animation_file: - csv_reader = csv.reader( - animation_file, delimiter=',', quotechar='|' - ) - try: - row_frame = csv_reader.next() - except: - return float('nan'), float('nan') - if len(row_frame) == 1: - anim_id = row_frame[0] - logger.debug("Got animation_id: {}".format(row_frame[0])) - row_frame = csv_reader.next() - if len(row_frame) == 2: - logger.debug("Got frame delay: {}".format(row_frame[1])) - row_frame - csv_reader.next() - try: - frame_number, x, y, z, yaw, red, green, blue = row_frame - except: - return float('nan'), float('nan') - return float(x)*x_ratio, float(y)*y_ratio + def load_csv_row(self, csv_row): + number, x, y, z, yaw, red, green, blue = csv_row + self.number = int(number) + self.x = float(x) + self.y = float(y) + self.z = float(z) + self.yaw = float(yaw) + self.red = int(red) + self.green = int(green) + self.blue = int(blue) + def get_pos(self): + if None in [self.x, self.y, self.z]: + return [] + else: + return [self.x, self.y, self.z] -def load_animation(filepath="animation.csv", default_delay = 0.1, x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1): - imported_frames = [] - global anim_id - try: - animation_file = open(filepath) - except IOError: - logger.debug("File {} can't be opened".format(filepath)) - anim_id = "No animation" - else: - with animation_file: - current_frame_delay = default_delay - csv_reader = csv.reader( - animation_file, delimiter=',', quotechar='|' - ) - row_0 = csv_reader.next() - if len(row_0) == 1: - anim_id = row_0[0] - logger.debug("Got animation_id: {}".format(anim_id)) - elif len(row_0) == 2: - current_frame_delay = float(row_0[1]) - logger.debug("Got new frame delay: {}".format(current_frame_delay)) - else: - logger.debug("No animation id in file") - frame_number, x, y, z, yaw, red, green, blue = row_0 - imported_frames.append({ - 'number': int(frame_number), - 'x': x_ratio*float(x) + x0, - 'y': y_ratio*float(y) + y0, - 'z': z_ratio*float(z) + z0, - 'yaw': float(yaw), - 'red': int(red), - 'green': int(green), - 'blue': int(blue), - 'delay': current_frame_delay - }) - for row in csv_reader: - if len(row) == 2: - current_frame_delay = float(row[1]) + def get_color(self): + if None in [self.red, self.green, self.blue]: + return [] + else: + return [self.red, self.green, self.blue] + + def set_yaw(self, yaw): + if yaw != "animation": + self.yaw = math.radians(float(yaw)) + + def pose_is_valid(self): + return self.get_pos() and (self.yaw is not None) + +class Animation(object): + def __init__(self, config=None, filepath="animation.csv"): + self.reset(filepath) + if config is not None: + self.update_frames(config, filepath) + + def reset(self, filepath): + self.id = None + self.start_delay = 0 + self.original_frames = None + self.static_begin_frames = None + self.static_begin_time = 0 + self.takeoff_frames = None + self.takeoff_time = 0 + self.route_frames = None + self.route_time = 0 + self.land_frames = None + self.land_time = 0 + self.static_end_frames = None + self.static_end_time = 0 + self.output_frames = None + self.output_frames_min_z = None + self.filepath = filepath + self.state = None + + def load(self, filepath="animation.csv", config = None): + self.original_frames = [] + self.corrected_frames = [] + self.filepath = filepath + self.state = "OK" + delay = config.animation_frame_delay + try: + animation_file = open(filepath) + except IOError: + logger.debug("File {} can't be opened".format(filepath)) + self.state = "No animation" + else: + with animation_file: + current_frame_delay = delay + csv_reader = csv.reader( + animation_file, delimiter=',', quotechar='|' + ) + row_0 = csv_reader.next() + if len(row_0) == 1: + self.id = row_0[0] + logger.debug("Got animation_id: {}".format(self.id)) + elif len(row_0) == 2: + current_frame_delay = float(row_0[1]) + logger.debug("Got new frame delay: {}".format(current_frame_delay)) else: - frame_number, x, y, z, yaw, red, green, blue = row - imported_frames.append({ - 'number': int(frame_number), - 'x': x_ratio*float(x) + x0, - 'y': y_ratio*float(y) + y0, - 'z': z_ratio*float(z) + z0, - 'yaw': float(yaw), - 'red': int(red), - 'green': int(green), - 'blue': int(blue), - 'delay': current_frame_delay - }) - return imported_frames + logger.debug("No animation id in file") + self.id = "No animation id" + try: + frame = Frame(row_0, current_frame_delay) + except ValueError as e: + logger.error("Can't parse row in csv file. {}".format(e)) + self.state = "Bad animation file" + return + try: + frame.set_yaw(config.animation_yaw) + except ValueError as e: + logger.error("Can't set yaw from configuration") + self.state = "Bad yaw from config" + return + self.original_frames.append(frame) + for row in csv_reader: + if len(row) == 2: + current_frame_delay = float(row[1]) + logger.debug("Got new frame delay: {}".format(current_frame_delay)) + else: + try: + frame = Frame(row, current_frame_delay) + except ValueError as e: + logger.error("Can't parse row in csv file. {}".format(e)) + self.state = "Bad animation file" + return + try: + frame.set_yaw(config.animation_yaw) + except ValueError as e: + logger.error("Can't set yaw from configuration") + self.state = "Bad yaw from config" + return + self.original_frames.append(frame) + self.split_animation() -def correct_animation(frames, frame_delay=0.1, min_takeoff_height=0.5, move_delta=0.01, check_takeoff=True, check_land=True): - corrected_frames = copy.deepcopy(frames) - start_action = 'takeoff' - frames_to_start = 0 - time_to_start = 0 - if len(corrected_frames) == 0: - raise Exception('Nothing to correct!') - # Check takeoff - # If copter takes off in animation file, copter must be armed first and then all animation can be played - if (corrected_frames[0]['z'] < min_takeoff_height) and check_takeoff: - start_action = 'arm' - # If the first point is low, then detect moment to arm, - # delete all points, where copter is standing, and count time_delta - for i in range(len(corrected_frames)-1): - if corrected_frames[i-frames_to_start+1]['z'] - corrected_frames[i-frames_to_start]['z'] > move_delta: + ''' + Split animation into 5 parts: static_begin, takeoff, route, land, static_end + * static_begin and static_end are chains of frames in the beginning and the end of animation, + where the drone doesn't move + * takeoff and land are chains of frames after and before static frames of animation, + where the drone doesn't move in xy plane, and it's z coordinate only increases or decreases, respectively. + * route is the rest of the animation + Count static_begin_time and takeoff_time + ''' + def split_animation(self, move_delta=0.01): + self.static_begin_frames = [] + self.takeoff_frames = [] + self.route_frames = [] + self.land_frames = [] + self.static_end_frames = [] + self.static_begin_time = 0 + self.takeoff_time = 0 + self.route_time = 0 + self.land_time = 0 + if len(self.original_frames) == 0: + return + frames = copy.deepcopy(self.original_frames) + i = 0 # Moving index from the beginning + # Select static begin frames + while i < len(frames) - 1: + self.static_begin_time += frames[i].delay + if moving(frames[i], frames[i+1], move_delta): break - time_to_start += corrected_frames[i-frames_to_start]['delay'] - del corrected_frames[i-frames_to_start] - frames_to_start += 1 - start_delay = time_to_start - # Check Land - # If copter lands in animation, landing points can be deleted - if (corrected_frames[len(corrected_frames)-1]['z'] < min_takeoff_height) and check_land: - for i in range(len(corrected_frames)-1,0,-1): - # print i - if abs(corrected_frames[i-1]['z'] - corrected_frames[i]['z']) < move_delta: + i += 1 + if i > 0: + self.static_begin_frames = frames[:i+1] + frames = frames[i+1:] + i = 0 + else: + self.static_begin_time = 0 + # Select takeoff frames + while i < len(frames) - 1: + self.takeoff_time += frames[i].delay + if moving(frames[i], frames[i+1], move_delta, z = False) or (frames[i+1].z - frames[i].z <= 0): break - del corrected_frames[i] - for i in range(len(corrected_frames)-1,0,-1): - if (abs(corrected_frames[i-1]['x'] - corrected_frames[i]['x']) > move_delta or - abs(corrected_frames[i-1]['y'] - corrected_frames[i]['y']) > move_delta): + i += 1 + if i > 0: + self.takeoff_frames = frames[:i+1] + frames = frames[i+1:] + else: + self.takeoff_time = 0 + i = len(frames) - 1 # Moving index from the end + # Select static end frames + while i >= 0: + self.static_end_time += frames[i].delay + if moving(frames[i], frames[i-1], move_delta): break - del corrected_frames[i] - return corrected_frames, start_action, start_delay + i -= 1 + if i < len(frames) - 1: + self.static_end_frames = frames[i+1:] + frames = frames[:i+1] + i = len(frames) - 1 + else: + self.static_end_time = 0 + # Select land frames + while i >= 0: + self.land_time += frames[i].delay + if moving(frames[i], frames[i-1], move_delta, z = False) or (frames[i-1].z - frames[i].z <= 0): + break + i -= 1 + if i < len(frames) - 1: + self.land_frames = frames[i+1:] + frames = frames[:i+1] + else: + self.land_time = 0 + # Get route frames + self.route_frames = frames + for frame in self.route_frames: + self.route_time += frame.delay -# Needs for test -def save_corrected_animation(frames, filename="corrected_animation.csv"): - corrected_animation = open(filename, mode='w+') - csv_writer = csv.writer(corrected_animation, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) - for frame in frames: - csv_writer.writerow([frame['number'],frame['x'], frame['y'], frame['z'], frame['delay']]) - # print frame - corrected_animation.close() + def make_output_frames(self, static_begin, takeoff, route, land, static_end): + self.output_frames = [] + self.output_frames_min_z = None + self.start_delay = 0. + if not self.original_frames: + return + if not static_begin and not takeoff and not route and not land and not static_end: + return + if static_begin: + self.output_frames += self.static_begin_frames + if not static_begin: + self.start_delay += self.static_begin_time + if takeoff: + self.output_frames += self.takeoff_frames + if not static_begin and not takeoff: + self.start_delay += self.takeoff_time + if route: + self.output_frames += self.route_frames + if not static_begin and not takeoff and not route: + self.start_delay += self.route_time + if land: + self.output_frames += self.land_frames + if not static_begin and not takeoff and not route and not land: + self.start_delay += self.land_time + if static_end: + self.output_frames += self.static_end_frames + if self.output_frames: + self.output_frames_min_z = min(self.output_frames, key = lambda p: p.z).z -def convert_frame(frame): - return ((frame['x'], frame['y'], frame['z']), (frame['red'], frame['green'], frame['blue']), frame['yaw']) + def update_frames(self, config, filepath): + self.reset(filepath) + self.load(filepath, config) + self.make_output_frames(config.animation_output_static_begin, + config.animation_output_takeoff, + config.animation_output_route, + config.animation_output_land, + config.animation_output_static_end) + + def get_scaled_output(self, ratio=(1,1,1), offset=(0,0,0)): + x0, y0, z0 = offset + x_ratio, y_ratio, z_ratio = ratio + scaled_frames = copy.deepcopy(self.output_frames) + if scaled_frames: + for frame in scaled_frames: + frame.x = x_ratio*frame.x + x0 + frame.y = y_ratio*frame.y + y0 + frame.z = z_ratio*frame.z + z0 + return scaled_frames + + def get_scaled_output_min_z(self, ratio=(1,1,1), offset=(0,0,0)): + if self.output_frames_min_z is None: + return None + z0 = offset[2] + z_ratio = ratio[2] + return self.output_frames_min_z*z_ratio + z0 + + def get_start_point(self, ratio=(1,1,1), offset=(0,0,0)): + if not self.output_frames: + return [] + x0, y0, z0 = offset + x_ratio, y_ratio, z_ratio = ratio + first_frame = self.output_frames[0] + x = x_ratio*first_frame.x + x0 + y = y_ratio*first_frame.y + y0 + z = z_ratio*first_frame.z + z0 + return [x, y, z] + + def get_start_yaw(self): + if not self.output_frames: + return float('nan') + return math.degrees(self.output_frames[0].yaw) + + def check_ground(self, ground_level=0, ratio=(1,1,1), offset=(0,0,0)): + return ground_level <= self.get_scaled_output_min_z(ratio, offset) + + def get_start_action(self, start_action, current_height, takeoff_level, + ground_level, ratio=(1,1,1), offset=(0,0,0)): + # Check output frames + if not self.output_frames: + return 'error: empty output frames' + if math.isnan(current_height): + return 'error: bad current_height' + # Check that bottom point of animation is higher than ground level + if ground_level > self.get_scaled_output_min_z(ratio, offset): + return 'error: some animation points are lower than ground level' + # Select start action + if start_action == 'auto': + if self.get_start_point(ratio, offset)[2] - current_height > takeoff_level: + return 'takeoff' + else: + return 'fly' + elif start_action in ('takeoff', 'fly'): + return start_action + else: + return 'error' + + # Need for tests + def save_corrected_animation(self): + name, ext = os.path.splitext(self.filepath) + filepath = name + '_corrected' + ext + with open(filepath, mode='w+') as corrected_animation: + csv_writer = csv.writer(corrected_animation, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + for frame in self.corrected_frames: + csv_writer.writerow([frame.number, frame.x, frame.y, frame.z, frame.red, frame.green, frame.blue, frame.delay]) try: - def execute_frame(point=(), color=(), yaw=float('Nan'), frame_id='aruco_map', use_leds=True, + def execute_frame(frame, frame_id='aruco_map', use_leds=True, flight_func=FlightLib.navto, auto_arm=False, flight_kwargs=None, interrupter=interrupt_event): if flight_kwargs is None: flight_kwargs = {} - - flight_func(*point, yaw=yaw, frame_id=frame_id, auto_arm=auto_arm, interrupter=interrupt_event, **flight_kwargs) + if frame.pose_is_valid(): + flight_func(x=frame.x, y=frame.y, z=frame.z, yaw=frame.yaw, frame_id=frame_id, auto_arm=auto_arm, interrupter=interrupt_event, **flight_kwargs) + else: + logger.debug("Frame pose is not valid for flying") if use_leds: - if color: + if frame.get_color: LedLib.fill(*color) - - - - def execute_animation(frames, frame_delay, frame_id='aruco_map', use_leds=True, flight_func=FlightLib.navto, - interrupter=interrupt_event): - next_frame_time = 0 - for frame in frames: - if interrupter.is_set(): - logger.warning("Animation playing function interrupted!") - interrupter.clear() - return - execute_frame(*convert_frame(frame), frame_id=frame_id, use_leds=use_leds, flight_func=flight_func, - interrupter=interrupter) - - next_frame_time += frame_delay - tasking.wait(next_frame_time, interrupter) - - def takeoff(z=1.5, safe_takeoff=True, frame_id='map', timeout=5.0, use_leds=True, interrupter=interrupt_event): if use_leds: diff --git a/Drone/config/spec/configspec_client.ini b/Drone/config/spec/configspec_client.ini index d054bf0..1574baa 100644 --- a/Drone/config/spec/configspec_client.ini +++ b/Drone/config/spec/configspec_client.ini @@ -39,17 +39,16 @@ decrease_thrust_after = float(default=5.0, min=0) [COPTER] frame_id = string(default=map) +arming_time = float(default=0.5) takeoff_height = float(default=1.0) takeoff_time = float(default=5.0, min=0) -safe_takeoff = boolean(default=False) reach_first_point_time = float(default=5.0, min=0) -land_time = float(default=1.0, min=0) +land_delay = float(default=1.0, min=0) land_timeout = float(default=10.0, min=0) # __list__ x y z common_offset = float_list(default=list(0, 0, 0), min=3, max=3) [FLOOR FRAME] -enabled = boolean(default=False) parent = string(default=map) # Frame translation (x, y, z) # __list__ x y z @@ -58,20 +57,41 @@ translation = float_list(default=list(0.0, 0.0, 0.0), min=3, max=3) # __list__ roll pitch yaw rotation = float_list(default=list(0.0, 0.0, 0.0), min=3, max=3) +[GPS FRAME] +lat = string(default=0) +lon = string(default=0) +yaw = float(default=0) + [ANIMATION] -takeoff_detection = boolean(default=True) -land_detection = boolean(default=True) +# Available options: +# 'auto' - automatic action selection from 'takeoff' or 'fly' based on current copter level +# 'takeoff' - takeoff to first output animation point after static_begin_time then execute 'takeoff logic' +# 'fly' - execute animation frames after static_begin_time +start_action = string(default=auto) +takeoff_level = float(default=0.5) +ground_level = float(default=0) frame_delay = float(default=0.1, min=0.01) # Animation ratio (x, y, z) # __list__ x y z ratio = float_list(default=list(1.0, 1.0, 1.0), min=3, max=3) -# Available options: 'animation', 'nan' or a number in degrees +# Available options: +# 'animation' - take yaw from animation +# 'nan' - don't change yaw during flight +# or a number in degrees yaw = string(default=180.0) +[[OUTPUT]] +static_begin = boolean(default=False) +takeoff = boolean(default=True) +route = boolean(default=True) +land = boolean(default=False) +static_end = boolean(default=False) [LED] use = boolean(default=False) pin = integer(default=21, min=0, max=100) count = integer(default=60, min=1) +takeoff_indication = boolean(default=True) +land_indication = boolean(default=True) [PRIVATE] # Available options: /hostname ; /default ; /ip ; any string 63 characters length diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 4e92baa..5c87562 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -9,7 +9,10 @@ import numpy try: from clever import srv except ImportError: - from clover import srv + try: + from clover import srv + except ImportError: + print("Can't import clever or clover") import datetime import logging @@ -17,8 +20,17 @@ import threading import psutil import subprocess from collections import namedtuple +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler -from FlightLib import FlightLib +try: + from FlightLib import FlightLib +except ImportError: + print("Can't import FlightLib") +try: + from FlightLib import LedLib +except ImportError: + print("Can't import LedLib") import client @@ -33,7 +45,26 @@ from geometry_msgs.msg import Point, Quaternion, TransformStamped from tf.transformations import quaternion_from_euler, euler_from_quaternion, quaternion_multiply import tf2_ros -static_bloadcaster = tf2_ros.StaticTransformBroadcaster() +from geographiclib.geodesic import Geodesic + +Earth = Geodesic.WGS84 + +def dist(x, y): + return math.sqrt(x**2+y**2) + +def azi(x, y): + return 90 - math.atan2(y,x)*180/math.pi + +def get_xy(dist, azi): + return dist*math.sin(math.radians(azi)), dist*math.cos(math.radians(azi)) + +def valid(pos): + for coord in pos: + if math.isnan(coord): + return False + return True + +static_broadcaster = tf2_ros.StaticTransformBroadcaster() emergency = False @@ -83,7 +114,8 @@ class CopterClient(client.Client): def __init__(self, config_path="config/client.ini"): super(CopterClient, self).__init__(config_path) self.load_config() - self.frames = {} + self.telemetry = None + self.animation = animation.Animation(self.config, "animation.csv") def load_config(self): super(CopterClient, self).load_config() @@ -97,18 +129,22 @@ class CopterClient(client.Client): if self.config.led_use: from FlightLib import LedLib LedLib.init_led(self.config.led_pin) - task_manager_instance.start() # TODO move to self - if self.config.copter_frame_id == "floor": - if self.config.floor_frame_enabled: - self.start_floor_frame_broadcast() - else: - rospy.logerr("Can't make floor frame!") + task_manager_instance.start() start_subscriber() - - telemetry.start_loop() - super(CopterClient, self).start() + self.telemetry = Telemetry() + self.telemetry.start_loop() + if self.config.copter_frame_id == "floor": + self.start_floor_frame_broadcast() + elif self.config.copter_frame_id == "gps": + self.start_gps_frame_broadcast() + client_thread = threading.Thread(target=super(CopterClient, self).start, name="Client thread") + client_thread.daemon = True + client_thread.start() + #super(CopterClient, self).start() def start_floor_frame_broadcast(self): + if self.config.floor_frame_parent == "gps": + self.start_gps_frame_broadcast() trans = TransformStamped() trans.transform.translation.x = self.config.floor_frame_translation[0] trans.transform.translation.y = self.config.floor_frame_translation[1] @@ -118,7 +154,48 @@ class CopterClient(client.Client): math.radians(self.config.floor_frame_rotation[2]))) trans.header.frame_id = self.config.floor_frame_parent trans.child_frame_id = self.config.copter_frame_id - static_bloadcaster.sendTransform(trans) + static_broadcaster.sendTransform(trans) + + def start_gps_frame_broadcast(self): + trans = TransformStamped() + trans.transform.translation.x = 0 + trans.transform.translation.y = 0 + trans.transform.translation.z = 0 + trans.transform.rotation = Quaternion(*quaternion_from_euler(0,0,0)) + trans.header.frame_id = "map" + trans.child_frame_id = "gps" + static_broadcaster.sendTransform(trans) + gps_frame_thread = threading.Thread(target=self.gps_frame_broadcast_loop, name="GPS frame broadcast thread") + gps_frame_thread.daemon = True + gps_frame_thread.start() + + def gps_frame_broadcast_loop(self): + rate = rospy.Rate(1) + while not rospy.is_shutdown(): + telem = FlightLib.get_telemetry_locked(frame_id = "map") + if telem is not None: + if math.isnan(telem.lat) or math.isnan(telem.lon) or math.isnan(telem.x) or math.isnan(telem.y): + logger.info("Can't get position from telemetry") + else: + lat = float(self.config.gps_frame_lat) + lon = float(self.config.gps_frame_lon) + geo_delta = Earth.Inverse(telem.lat, telem.lon, lat, lon) + #logger.info("dist: {} | azi: {}".format(geo_delta['s12'], geo_delta['azi1'])) + dx, dy = get_xy(geo_delta['s12'], geo_delta['azi1']) + gps_dx = telem.x + dx + gps_dy = telem.y + dy + #logger.info("GPS frame dx: {} | dy: {}".format(gps_dx, gps_dy)) + trans = TransformStamped() + trans.transform.translation.x = gps_dx + trans.transform.translation.y = gps_dy + trans.transform.translation.z = 0 + trans.transform.rotation = Quaternion(*quaternion_from_euler(0,0, + math.radians(self.config.gps_frame_yaw))) + trans.header.frame_id = "map" + trans.child_frame_id = "gps" + static_broadcaster.sendTransform(trans) + + rate.sleep() def restart_service(name): @@ -259,13 +336,13 @@ def _execute(*args, **kwargs): def _response_id(*args, **kwargs): new_id = kwargs.get("new_id", None) if new_id is not None: - old_id = client.active_client.client_id + old_id = copter.client_id if new_id != old_id: - client.active_client.config.set('PRIVATE', 'id', new_id, write=True) - client.active_client.client_id = new_id + copter.config.set('PRIVATE', 'id', new_id, write=True) + copter.client_id = new_id if new_id != '/hostname': - if client.active_client.config.system_restart_after_rename: - hostname = client.active_client.client_id + if copter.config.system_restart_after_rename: + hostname = copter.client_id configure_hostname(hostname) configure_hosts(hostname) configure_bashrc(hostname) @@ -292,28 +369,14 @@ def _response_selfcheck(*args, **kwargs): @messaging.request_callback("telemetry") def _response_telemetry(*args, **kwargs): - telemetry.update() - return telemetry.create_msg_contents() + copter.telemetry.update() + return copter.telemetry.create_msg_contents() @messaging.request_callback("anim_id") def _response_animation_id(*args, **kwargs): # Load animation - result = animation.get_id() - if result != 'No animation': - logger.debug("Saving corrected animation") - offset = numpy.array(client.active_client.config.private_offset) + numpy.array(client.active_client.config.copter_common_offset) - frames = animation.load_animation(os.path.abspath("animation.csv"), client.active_client.config.animation_frame_delay, - offset[0], offset[1], offset[2], *client.active_client.config.animation_ratio) - # Correct start and land frames in animation - corrected_frames, start_action, start_delay = animation.correct_animation(frames, - check_takeoff=client.active_client.config.animation_takeoff_detection, - check_land=client.active_client.config.animation_land_detection, - ) - logger.debug("Start action: {}".format(start_action)) - # Save corrected animation - animation.save_corrected_animation(corrected_frames) - return result + return copter.animation.id @messaging.request_callback("batt_voltage") @@ -350,9 +413,9 @@ def _response_cal_status(*args, **kwargs): @messaging.request_callback("position") def _response_position(*args, **kwargs): - telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id) + telem = copter.telemetry.ros_telemetry return "{:.2f} {:.2f} {:.2f} {:.1f} {}".format( - telem.x, telem.y, telem.z, math.degrees(telem.yaw), client.active_client.config.copter_frame_id) + telem.x, telem.y, telem.z, math.degrees(telem.yaw), copter.config.copter_frame_id) @messaging.request_callback("calibrate_gyro") @@ -381,60 +444,61 @@ def _command_test(*args, **kwargs): print("stdout test") +@messaging.message_callback("update_animation") +def _command_update_animation(*args, **kwargs): + copter.animation.update_frames(copter.config, "animation.csv") + + @messaging.message_callback("move_start") def _command_move_start_to_current_position(*args, **kwargs): - x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"), - *client.active_client.config.animation_ratio) - logger.debug("x_start = {}, y_start = {}".format(x_start, y_start)) - if not math.isnan(x_start): - telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id) - logger.debug("x_telem = {}, y_telem = {}".format(telem.x, telem.y)) - if not math.isnan(telem.x): - client.active_client.config.set('PRIVATE', 'offset', - [telem.x - x_start, telem.y - y_start, client.active_client.config.private_offset[2]], - write=True) - logger.info("Set start delta: {:.2f} {:.2f}".format(client.active_client.config.private_offset[0], - client.active_client.config.private_offset[1])) - else: - logger.debug("Wrong telemetry") + offset = numpy.array(copter.config.private_offset) + numpy.array(copter.config.copter_common_offset) + try: + xs, ys, zs = copter.animation.get_start_point(copter.config.ratio, offset) + except ValueError: + logger.error("Can't get start point. Check animation file!") else: - logger.debug("Wrong animation file") + logger.debug("start x = {}, y = {}".format(xs, ys)) + telem = copter.telemetry.ros_telemetry + logger.debug("telemetry x = {}, y = {}".format(telem.x, telem.y)) + if valid([telem.x, telem.y, telem.z]): + copter.config.set('PRIVATE', 'offset', + [telem.x - xs, telem.y - ys, copter.config.private_offset[2]], write=True) + logger.info("Set start delta: {:.2f} {:.2f}".format(copter.config.private_offset[0], + copter.config.private_offset[1])) + else: + logger.error("Wrong telemetry") @messaging.message_callback("reset_start") def _command_reset_start(*args, **kwargs): - client.active_client.config.set('PRIVATE', 'offset', - [0, 0, client.active_client.config.private_offset[2]], - write=True) - logger.info("Reset start to {:.2f} {:.2f}".format(client.active_client.config.private_offset[0], - client.active_client.config.private_offset[1])) + copter.config.set('PRIVATE', 'offset', + [0, 0, copter.config.private_offset[2]], write=True) + logger.info("Reset start to {:.2f} {:.2f}".format(copter.config.private_offset[0], + copter.config.private_offset[1])) @messaging.message_callback("set_z_to_ground") def _command_set_z(*args, **kwargs): - telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id) - client.active_client.config.set('PRIVATE', 'offset', - [client.active_client.config.private_offset[0], client.active_client.config.private_offset[1], telem.z], - write=True) - logger.info("Set z offset to {:.2f}".format(client.active_client.config.private_offset[2])) + telem = copter.telemetry.ros_telemetry + if valid([telem.x, telem.y, telem.z]): + copter.config.set('PRIVATE', 'offset', + [copter.config.private_offset[0], copter.config.private_offset[1], telem.z], write=True) + logger.info("Set z offset to {:.2f}".format(copter.config.private_offset[2])) + else: + logger.error("Wrong telemetry") @messaging.message_callback("reset_z_offset") def _command_reset_z(*args, **kwargs): - client.active_client.config.set('PRIVATE', 'offset', - [client.active_client.config.private_offset[0], client.active_client.config.private_offset[1], 0], - write=True) - logger.info("Reset z offset to {:.2f}".format(client.active_client.config.private_offset[2])) + copter.config.set('PRIVATE', 'offset', + [copter.config.private_offset[0], copter.config.private_offset[1], 0], write=True) + logger.info("Reset z offset to {:.2f}".format(copter.config.private_offset[2])) @messaging.message_callback("update_repo") def _command_update_repo(*args, **kwargs): - os.system("mv /home/pi/clever-show/drone/client_config.ini /home/pi/clever-show/drone/client_config_tmp.ini") - os.system("git reset --hard HEAD") - os.system("git checkout master") os.system("git fetch") os.system("git pull --rebase") - os.system("mv /home/pi/clever-show/drone/client_config_tmp.ini /home/pi/clever-show/drone/client_config.ini") os.system("chown -R pi:pi /home/pi/clever-show") @@ -452,12 +516,16 @@ def _command_reboot(*args, **kwargs): @messaging.message_callback("service_restart") def _command_service_restart(*args, **kwargs): service = kwargs["name"] + if service=="clover": + restart_service("clever") + if service=="clever-show": + restart_service("clever-show@{}".format(copter.client_id)) restart_service(service) @messaging.message_callback("repair_chrony") def _command_chrony_repair(*args, **kwargs): - repair_chrony(client.active_client.config.server_host) + repair_chrony(copter.config.server_host) @messaging.message_callback("led_test") @@ -472,13 +540,12 @@ def _command_led_fill(*args, **kwargs): r = kwargs.get("red", 0) g = kwargs.get("green", 0) b = kwargs.get("blue", 0) - LedLib.fill(r, g, b) @messaging.message_callback("flip") def _copter_flip(*args, **kwargs): - FlightLib.flip(frame_id=client.active_client.config.copter_frame_id) + FlightLib.flip(frame_id=copter.config.copter_frame_id) @messaging.message_callback("takeoff") @@ -486,30 +553,36 @@ def _command_takeoff(*args, **kwargs): logger.info("Takeoff at {}".format(datetime.datetime.now())) task_manager.add_task(0, 0, animation.takeoff, task_kwargs={ - "z": client.active_client.config.copter_takeoff_height, - "timeout": client.active_client.config.copter_takeoff_time, - "safe_takeoff": client.active_client.config.copter_safe_takeoff, - "use_leds": client.active_client.config.led_use, - } - ) + "z": copter.config.copter_takeoff_height, + "timeout": copter.config.copter_takeoff_time, + "safe_takeoff": False, + "use_leds": copter.config.led_use & copter.config.led_takeoff_indication, + }) @messaging.message_callback("takeoff_z") def _command_takeoff_z(*args, **kwargs): - z_str = kwargs.get("z", None) - if z_str is not None: - telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id) - logger.info("Takeoff to z = {} at {}".format(z_str, datetime.datetime.now())) - task_manager.add_task(0, 0, FlightLib.reach_point, - task_kwargs={ - "x": telem.x, - "y": telem.y, - "z": float(z_str), - "frame_id": client.active_client.config.copter_frame_id, - "timeout": client.active_client.config.copter_takeoff_time, - "auto_arm": True, - } - ) + try: + z = float(kwargs.get("z", None)) + except TypeError: + logger.error("takeoff_z: No z argument!") + except ValueError: + logger.error("takeoff_z: Wrong z argument!") + else: + telem = FlightLib.get_telemetry_locked(copter.config.copter_frame_id) + if valid([telem.x, telem.y, telem.z]): + logger.info("Takeoff to z = {} at {}".format(z, datetime.datetime.now())) + task_manager.add_task(0, 0, FlightLib.reach_point, + task_kwargs={ + "x": telem.x, + "y": telem.y, + "z": z, + "frame_id": copter.config.copter_frame_id, + "timeout": copter.config.copter_takeoff_time, + "auto_arm": True, + }) + else: + logger.error("Wrong telemetry!") @messaging.message_callback("land") @@ -517,12 +590,11 @@ def _command_land(*args, **kwargs): task_manager.reset() task_manager.add_task(0, 0, animation.land, task_kwargs={ - "z": client.active_client.config.copter_takeoff_height, - "timeout": client.active_client.config.copter_takeoff_time, - "frame_id": client.active_client.config.copter_frame_id, - "use_leds": client.active_client.config.led_use, - } - ) + "z": copter.config.copter_takeoff_height, + "timeout": copter.config.copter_land_timeout, + "frame_id": copter.config.copter_frame_id, + "use_leds": copter.config.led_use & copter.config.led_land_indication, + }) @messaging.message_callback("emergency_land") @@ -536,8 +608,7 @@ def _command_disarm(*args, **kwargs): task_manager.add_task(-5, 0, FlightLib.arming_wrapper, task_kwargs={ "state": False - } - ) + }) @messaging.message_callback("stop") @@ -557,110 +628,107 @@ def _command_resume(*args, **kwargs): @messaging.message_callback("start") def _play_animation(*args, **kwargs): - start_time = float(kwargs["time"]) - # Check if animation file is available - if animation.get_id() == 'No animation': - logger.error("Can't start animation without animation file!") + + # Validate start_time + try: + start_time = float(kwargs["time"]) + except ValueError: + logger.error("start: Wrong time argument!") + return + except KeyError: + logger.error("start: No time argument!") return + # Check animation state + if copter.animation.state is not "OK": + logger.error("start: Bad animation state") + return + + # Get output frames + offset = numpy.array(copter.config.private_offset) + numpy.array(copter.config.copter_common_offset) + frames = copter.animation.get_scaled_output(copter.config.animation_ratio, offset) + if not frames: + logger.error("start: No frames in animation!") + return + + # Get current telemetry + telem = copter.telemetry.ros_telemetry + if not valid([telem.x, telem.y, telem.z]): + logger.error("start: Position is not valid!") + return + + # Get start action and delay + try: + start_action, start_delay = copter.telemetry.start_position[-2:] + except ValueError: + logger.error("start: Can't get animation start position and delay") + return + # Reset task manager task_manager.reset(interrupt_next_task=False) - logger.info("Start time = {}, wait for {} seconds".format(start_time, start_time - time.time())) - # Load animation - offset = numpy.array(client.active_client.config.private_offset) + numpy.array(client.active_client.config.copter_common_offset) - frames = animation.load_animation(os.path.abspath("animation.csv"), client.active_client.config.animation_frame_delay, - offset[0], offset[1], offset[2], *client.active_client.config.animation_ratio) - # Correct start and land frames in animation - corrected_frames, start_action, start_delay = animation.correct_animation(frames, - check_takeoff=client.active_client.config.animation_takeoff_detection, - check_land=client.active_client.config.animation_land_detection, - ) - # Choose start action + # Set animation logic if start_action == 'takeoff': - # Takeoff first - task_manager.add_task(start_time, 0, animation.takeoff, + # Takeoff first at start_time + start_delay_time + takeoff_time = start_time + start_delay + task_manager.add_task(takeoff_time, 0, animation.takeoff, task_kwargs={ - "z": client.active_client.config.copter_takeoff_height, - "timeout": client.active_client.config.copter_takeoff_time, - "safe_takeoff": client.active_client.config.copter_safe_takeoff, - # "frame_id": client.active_client.config.copter_frame_id, - "use_leds": client.active_client.config.led_use, - } - ) + "z": copter.config.copter_takeoff_height, + "timeout": copter.config.copter_takeoff_time, + "safe_takeoff": False, + "use_leds": copter.config.led_use & copter.config.led_takeoff_indication, + }) # Fly to first point - rfp_time = start_time + client.active_client.config.copter_takeoff_time + rfp_time = takeoff_time + copter.config.copter_takeoff_time task_manager.add_task(rfp_time, 0, animation.execute_frame, task_kwargs={ - "point": animation.convert_frame(corrected_frames[0])[0], - "color": animation.convert_frame(corrected_frames[0])[1], - "frame_id": client.active_client.config.copter_frame_id, - "use_leds": client.active_client.config.led_use, + "frame": frames[0], + "frame_id": copter.config.copter_frame_id, + "use_leds": copter.config.led_use, "flight_func": FlightLib.reach_point, - } - ) + }) # Calculate first frame start time - frame_time = rfp_time + client.active_client.config.copter_reach_first_point_time + frame_time = rfp_time + copter.config.copter_reach_first_point_time - elif start_action == 'arm': + elif start_action == 'fly': # Calculate start time - start_time += start_delay - # Arm - # task_manager.add_task(start_time, 0, FlightLib.arming_wrapper, - # task_kwargs={ - # "state": True - # } - # ) - frame_time = start_time # + 1.0 - point, color, yaw = animation.convert_frame(corrected_frames[0]) - task_manager.add_task(frame_time, 0, animation.execute_frame, + arm_time = start_time + start_delay # + 1.0 + task_manager.add_task(arm_time, 0, animation.execute_frame, task_kwargs={ - "point": point, - "color": color, - "frame_id": client.active_client.config.copter_frame_id, - "use_leds": client.active_client.config.led_use, + "frame": frames[0], + "frame_id": copter.config.copter_frame_id, + "use_leds": copter.config.led_use, "flight_func": FlightLib.navto, "auto_arm": True, - } - ) + }) # Calculate first frame start time - frame_time += corrected_frames[0]["delay"] # TODO Think about arming time - logger.debug(task_manager.task_queue) + frame_time = arm_time + copter.config.copter_arming_time + logger.debug("Start queue {}".format(task_manager.task_queue)) # Play animation file - for frame in corrected_frames: - point, color, yaw = animation.convert_frame(frame) - if client.active_client.config.animation_yaw == "animation": - yaw = frame["yaw"] - else: - yaw = math.radians(float(client.active_client.config.animation_yaw)) + for frame in frames: task_manager.add_task(frame_time, 0, animation.execute_frame, task_kwargs={ - "point": point, - "color": color, - "yaw": yaw, - "frame_id": client.active_client.config.copter_frame_id, - "use_leds": client.active_client.config.led_use, + "frame": frame, + "frame_id": copter.config.copter_frame_id, + "use_leds": copter.config.led_use, "flight_func": FlightLib.navto, - } - ) - frame_time += frame["delay"] - + }) + frame_time += frame.delay # Calculate land_time - land_time = frame_time + client.active_client.config.copter_land_time + land_time = frame_time + copter.config.copter_land_delay # Land task_manager.add_task(land_time, 0, animation.land, task_kwargs={ - "timeout": client.active_client.config.copter_land_timeout, - "frame_id": client.active_client.config.copter_frame_id, - "use_leds": client.active_client.config.led_use, - }, - ) + "timeout": copter.config.copter_land_timeout, + "frame_id": copter.config.copter_frame_id, + "use_leds": copter.config.led_use & copter.config.led_land_indication, + }) # noinspection PyAttributeOutsideInit class Telemetry: params_default_dict = { "git_version": None, - "animation_id": None, + "animation_info": None, "battery": None, "armed": False, "fcu_status": None, @@ -705,19 +773,25 @@ class Telemetry: @classmethod def get_config_version(cls): - return "{} V{}".format(client.active_client.config.config_name, client.active_client.config.config_version) + return "{} V{}".format(copter.config.config_name, copter.config.config_version) - @classmethod - def get_start_position(cls): - x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"), - *client.active_client.config.animation_ratio) - offset = numpy.array(client.active_client.config.private_offset) + numpy.array(client.active_client.config.copter_common_offset) - x = x_start + offset[0] - y = y_start + offset[1] - z = offset[2] - if not FlightLib._check_nans(x, y, z): - return x, y, z - return 'NO_POS' + def get_start_position(self): + offset = numpy.array(copter.config.private_offset) + numpy.array(copter.config.copter_common_offset) + try: + x, y, z = copter.animation.get_start_point(copter.config.animation_ratio, offset) + except ValueError: + return [float('nan'),float('nan'),float('nan'),float('nan'),'error: no start pos in animation',float('nan')] + else: + start_delay = copter.animation.start_delay + yaw = copter.animation.get_start_yaw() + if not self.ros_telemetry: + start_action = 'error: no telemetry data' + else: + start_action = copter.animation.get_start_action( + copter.config.animation_start_action, self.ros_telemetry.z, + copter.config.animation_takeoff_level, copter.config.animation_ground_level, + copter.config.animation_ratio, offset) + return [x,y,z,yaw,start_action,start_delay] @classmethod def get_battery(cls, ros_telemetry): @@ -751,16 +825,21 @@ class Telemetry: @classmethod def get_position(cls, ros_telemetry): - x, y, z = ros_telemetry.x, ros_telemetry.y, ros_telemetry.z + try: + x, y, z = ros_telemetry.x, ros_telemetry.y, ros_telemetry.z + except AttributeError: + return 'NO_POS' if not math.isnan(x): - return x, y, z, math.degrees(ros_telemetry.yaw), client.active_client.config.copter_frame_id + return x, y, z, math.degrees(ros_telemetry.yaw), copter.config.copter_frame_id return 'NO_POS' + def get_ros_telemetry(self): + return self.ros_telemetry + def update_telemetry_fast(self): - self.start_position = self.get_start_position() self.last_task = task_manager.get_current_task() try: - self.ros_telemetry = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id) + self.ros_telemetry = FlightLib.get_telemetry_locked(copter.config.copter_frame_id) if self.ros_telemetry.connected: self.armed = self.ros_telemetry.armed self.mode = self.ros_telemetry.mode @@ -779,9 +858,10 @@ class Telemetry: self.round_telemetry() def update_telemetry_slow(self): - self.animation_id = animation.get_id() + self.animation_info = [copter.animation.id, copter.animation.state] self.git_version = self.get_git_version() self.config_version = self.get_config_version() + self.start_position = self.get_start_position() try: self.calibration_status = get_calibration_status() self.fcu_status = get_sys_status() @@ -848,7 +928,7 @@ class Telemetry: def transmit_message(self): # todo if connected try: - client.active_client.server_connection.send_message('telemetry', kwargs={'value': self.create_msg_contents()}) + copter.server_connection.send_message('telemetry', kwargs={'value': self.create_msg_contents()}) except AttributeError as e: logger.debug(e) @@ -878,7 +958,7 @@ class Telemetry: self.update_telemetry_fast() self.check_failsafe_and_interruption() - if client.active_client.config.telemetry_transmit and client.active_client.connected: + if copter.config.telemetry_transmit and copter.connected: self.transmit_message() rate.sleep() @@ -887,18 +967,20 @@ class Telemetry: rate = rospy.Rate(1) while not rospy.is_shutdown(): self.update_telemetry_slow() - if client.active_client.config.telemetry_log_resources: + if copter.config.telemetry_log_resources: self.log_cpu_and_memory() rate.sleep() def start_loop(self): - if client.active_client.config.telemetry_frequency > 0: + if copter.config.telemetry_frequency > 0: telemetry_thread = threading.Thread(target=self._update_loop, name="Telemetry getting thread", - args=(client.active_client.config.telemetry_frequency,)) # TODO MOVE? Daemon? + args=(copter.config.telemetry_frequency,)) + telemetry_thread.daemon = True + telemetry_thread.start() slow_telemetry_thread = threading.Thread(target=self._slow_update_loop, name="Slow telemetry getting thread") + slow_telemetry_thread.daemon = True slow_telemetry_thread.start() - telemetry_thread.start() else: logger.info("Telemetry loop is not created because of zero or negative telemetry frequency") @@ -914,9 +996,25 @@ def emergency_callback(data): emergency = data.data +class AnimationEventHandler(FileSystemEventHandler): + def on_any_event(self, event): + logger.info('{} is {}'.format(event.src_path, event.event_type)) + # logger.info(os.path.splitext(event.src_path)) + if (os.path.splitext(event.src_path)[-1] == '.csv' and event.event_type != "deleted") or event.src_path.split('/')[-1] == 'client.ini': + if os.path.exists("animation.csv"): + logger.info("Update frames from animation.csv") + copter.animation.update_frames(copter.config, "animation.csv") + + if __name__ == "__main__": - telemetry = Telemetry() - copter_client = CopterClient() + copter = CopterClient() task_manager = tasking.TaskManager() rospy.Subscriber('/emergency', Bool, emergency_callback) - copter_client.start(task_manager) + event_handler = AnimationEventHandler() + observer = Observer() + observer.schedule(event_handler, ".", recursive=True) + observer.daemon = True + observer.start() + copter.start(task_manager) + while not rospy.is_shutdown(): + rospy.sleep(0.1) diff --git a/Drone/requirements.txt b/Drone/requirements.txt index f46bac3..260a5f8 100644 --- a/Drone/requirements.txt +++ b/Drone/requirements.txt @@ -1,3 +1,4 @@ selectors2 psutil configobj +watchdog diff --git a/Server/config/spec/configspec_server.ini b/Server/config/spec/configspec_server.ini index 7242d1d..6a191c8 100644 --- a/Server/config/spec/configspec_server.ini +++ b/Server/config/spec/configspec_server.ini @@ -17,17 +17,17 @@ config_version = float(default='1.0') [[[DEFAULT]]] copter_id = preset_param(default=list(True, 100)) git_version = preset_param(default=list(True, 75)) - config_version = preset_param(default=list(True, 140)) - animation_id = preset_param(default=list(True, 100)) + config_version = preset_param(default=list(True, 105)) + animation_info = preset_param(default=list(True, 100)) battery = preset_param(default=list(True, 100)) fcu_status = preset_param(default=list(True, 100)) calibration_status = preset_param(default=list(True, 65)) mode = preset_param(default=list(True, 100)) selfcheck = preset_param(default=list(True, 65)) current_position = preset_param(default=list(True, 250)) - start_position = preset_param(default=list(True, 150)) - last_task = preset_param(default=list(True, 250)) - time_delta = preset_param(default=list(True, 100)) + start_position = preset_param(default=list(True, 240)) + last_task = preset_param(default=list(True, 275)) + time_delta = preset_param(default=list(True, 70)) [[[__many__]]] __many__ = preset_param diff --git a/Server/copter_table.py b/Server/copter_table.py index 2a1df89..f216432 100644 --- a/Server/copter_table.py +++ b/Server/copter_table.py @@ -226,8 +226,8 @@ class CopterTableWidget(QTableView): @pyqtSlot(QtCore.QPoint) def open_menu(self, point): menu = QMenu(self) - index = self.indexAt(point) - item = self.model.get_row_data(index) + id = self.indexAt(point).siblingAtColumn(0).data() + item = self.model.get_row_by_attr('copter_id', id) edit_config = QAction("Edit config") edit_config.triggered.connect(partial(self.edit_copter_config, item)) diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py index 5af3894..aa4917a 100644 --- a/Server/copter_table_models.py +++ b/Server/copter_table_models.py @@ -81,16 +81,19 @@ class ModelChecks: def check_ver(item): if not ModelChecks.check_git: return True - + version = get_git_version() if version is not None: return version == item return True -@ModelChecks.column_check("animation_id") +@ModelChecks.column_check("animation_info") def check_anim(item): - return str(item) != 'No animation' + if item: + return str(item[1]) == 'OK' + else: + return False @ModelChecks.column_check("battery") @@ -142,6 +145,10 @@ def check_time_delta(item): @ModelChecks.column_check("start_position", pass_context=True) def check_start_pos(item, context): + if len(item) == 6: + if not item[4] in ["takeoff", "fly"]: + return False + if ModelChecks.start_pos_delta_max == 0: return True @@ -150,6 +157,7 @@ def check_start_pos(item, context): delta = get_distance(get_position(context.current_position), get_position(context.start_position)) + if math.isnan(delta): return False @@ -157,7 +165,7 @@ def check_start_pos(item, context): def get_position(position): - if position != 'NO_POS' and position[0] != 'nan': # float('nan')? + if not isinstance(position, str) and position[0] != float('nan'): return position[:3] return [float('nan')] * 3 @@ -276,6 +284,17 @@ def place_id(value): msgbox.exec_() return None +@ModelFormatter.view_formatter("animation_info") +def view_animation_info(value): + try: + id, state = value + except ValueError: + return "" + else: + if state == 'OK': + return id + else: + return state @ModelFormatter.place_formatter("battery") def place_battery(value): @@ -307,15 +326,18 @@ def view_selfcheck(value): def view_current_position(value): if isinstance(value, list): x, y, z, yaw, frame = value - return f"{x: .2f} {y: .2f} {z: .2f} {int(yaw): d} {frame}" + return f"{x: .2f} {y: .2f} {z: .2f} {yaw: .0f} {frame}" return value @ModelFormatter.view_formatter("start_position") def view_start_position(value): if isinstance(value, list): - x, y, z = value - return f"{x: .2f} {y: .2f} {z: .2f}" + x, y, z, yaw, action, delay = value + if action in ['fly', 'takeoff']: + return f"{x: .2f} {y: .2f} {z: .2f} {yaw: .0f} {action} {delay: .1f}" + else: + return f"{action}" return value @@ -340,14 +362,14 @@ class CopterDataModel(QtCore.QAbstractTableModel): columns_dict = {'copter_id': 'copter ID', 'git_version': 'version', 'config_version': 'configuration', - 'animation_id': ' animation ID ', + 'animation_info': 'animation ID', 'battery': ' battery ', 'fcu_status': 'FCU status', 'calibration_status': 'sensors', 'mode': ' mode ', 'selfcheck': ' checks ', 'current_position': 'current x y z yaw frame_id', - 'start_position': ' start x y z ', + 'start_position': 'start x y z yaw action delay', 'last_task': 'last task', 'time_delta': 'dt', } diff --git a/Server/server_gui.py b/Server/server_gui.py index 1acacc4..03dc120 100644 --- a/Server/server_gui.py +++ b/Server/server_gui.py @@ -13,7 +13,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.resize(1360, 816) + MainWindow.resize(1360, 869) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setEnabled(True) self.centralwidget.setObjectName("centralwidget") @@ -44,30 +44,22 @@ class Ui_MainWindow(object): self.verticalLayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize) self.verticalLayout.setObjectName("verticalLayout") self.formLayout = QtWidgets.QFormLayout() - self.formLayout.setLabelAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) + self.formLayout.setLabelAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) self.formLayout.setObjectName("formLayout") self.start_text = QtWidgets.QLabel(self.centralwidget) self.start_text.setLayoutDirection(QtCore.Qt.RightToLeft) - self.start_text.setAlignment(QtCore.Qt.AlignCenter) + self.start_text.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) self.start_text.setObjectName("start_text") self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.start_text) self.start_delay_spin = QtWidgets.QSpinBox(self.centralwidget) self.start_delay_spin.setObjectName("start_delay_spin") self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.start_delay_spin) - self.music_text = QtWidgets.QLabel(self.centralwidget) - self.music_text.setLayoutDirection(QtCore.Qt.RightToLeft) - self.music_text.setObjectName("music_text") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.music_text) self.music_delay_spin = QtWidgets.QDoubleSpinBox(self.centralwidget) self.music_delay_spin.setDecimals(1) self.music_delay_spin.setMaximum(1000.0) self.music_delay_spin.setObjectName("music_delay_spin") self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.music_delay_spin) - self.music_play_text = QtWidgets.QLabel(self.centralwidget) - self.music_play_text.setLayoutDirection(QtCore.Qt.RightToLeft) - self.music_play_text.setObjectName("music_play_text") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.music_play_text) self.music_checkbox = QtWidgets.QCheckBox(self.centralwidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -82,6 +74,14 @@ class Ui_MainWindow(object): self.music_checkbox.setChecked(False) self.music_checkbox.setObjectName("music_checkbox") self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.music_checkbox) + self.music_text = QtWidgets.QLabel(self.centralwidget) + self.music_text.setLayoutDirection(QtCore.Qt.RightToLeft) + self.music_text.setObjectName("music_text") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.music_text) + self.music_play_text = QtWidgets.QLabel(self.centralwidget) + self.music_play_text.setLayoutDirection(QtCore.Qt.RightToLeft) + self.music_play_text.setObjectName("music_play_text") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.music_play_text) self.verticalLayout.addLayout(self.formLayout) self.line = QtWidgets.QFrame(self.centralwidget) self.line.setFrameShape(QtWidgets.QFrame.HLine) @@ -212,7 +212,7 @@ class Ui_MainWindow(object): self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1360, 25)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1360, 22)) self.menubar.setObjectName("menubar") self.menuOptions = QtWidgets.QMenu(self.menubar) self.menuOptions.setObjectName("menuOptions") @@ -301,6 +301,8 @@ class Ui_MainWindow(object): self.action_configure_columns.setObjectName("action_configure_columns") self.actionSomething = QtWidgets.QAction(MainWindow) self.actionSomething.setObjectName("actionSomething") + self.action_send_animation = QtWidgets.QAction(MainWindow) + self.action_send_animation.setObjectName("action_send_animation") self.menuMusic_2.addAction(self.action_select_music_file) self.menuMusic_2.addAction(self.action_play_music) self.menuMusic_2.addAction(self.action_stop_music) @@ -319,10 +321,12 @@ class Ui_MainWindow(object): self.menuTable.addSeparator() self.menuTable.addAction(self.action_configure_columns) self.menuSend.addAction(self.action_send_animations) + self.menuSend.addAction(self.action_send_calibrations) + self.menuSend.addSeparator() + self.menuSend.addAction(self.action_send_aruco_map) + self.menuSend.addAction(self.action_send_animation) self.menuSend.addAction(self.action_send_configurations) self.menuSend.addAction(self.action_send_launch_file) - self.menuSend.addAction(self.action_send_aruco_map) - self.menuSend.addAction(self.action_send_calibrations) self.menuSend.addAction(self.action_send_fcu_parameters) self.menuSend.addSeparator() self.menuSend.addAction(self.action_send_any_file) @@ -357,8 +361,8 @@ class Ui_MainWindow(object): MainWindow.setWindowTitle(_translate("MainWindow", "Clever Drone Show")) self.start_text.setText(_translate("MainWindow", " Start after")) self.start_delay_spin.setSuffix(_translate("MainWindow", " s")) - self.music_text.setText(_translate("MainWindow", " Music after")) self.music_delay_spin.setSuffix(_translate("MainWindow", " s")) + self.music_text.setText(_translate("MainWindow", " Music after")) self.music_play_text.setText(_translate("MainWindow", " Play music")) self.check_button.setText(_translate("MainWindow", "Preflight check")) self.start_button.setText(_translate("MainWindow", "Start animation")) @@ -389,7 +393,7 @@ class Ui_MainWindow(object): self.action_send_aruco_map.setText(_translate("MainWindow", "Aruco map")) self.action_update_client_repo.setText(_translate("MainWindow", "Update clever-show git")) self.actionSend_launch_file_for_clever.setText(_translate("MainWindow", "Send launch file for clever")) - self.action_send_launch_file.setText(_translate("MainWindow", "Launch files")) + self.action_send_launch_file.setText(_translate("MainWindow", "Launch files folder")) self.action_restart_clever.setText(_translate("MainWindow", "clever")) self.action_restart_clever_show.setText(_translate("MainWindow", "clever-show")) self.action_select_all_rows.setText(_translate("MainWindow", "Select all drones")) @@ -410,7 +414,7 @@ class Ui_MainWindow(object): self.action_send_calibrations.setText(_translate("MainWindow", "Camera calibrations")) self.action_reboot_all.setText(_translate("MainWindow", "Reboot")) self.action_restart_chrony.setText(_translate("MainWindow", "chrony")) - self.action_send_fcu_parameters.setText(_translate("MainWindow", "FCU parameters")) + self.action_send_fcu_parameters.setText(_translate("MainWindow", "FCU parameters file")) self.action_toggle_select.setText(_translate("MainWindow", "Toggle select")) self.action_toggle_select.setShortcut(_translate("MainWindow", "Ctrl+A")) self.action_select_all.setText(_translate("MainWindow", "Select all")) @@ -424,3 +428,4 @@ class Ui_MainWindow(object): self.action_restart_server.setText(_translate("MainWindow", "Restart server")) self.action_configure_columns.setText(_translate("MainWindow", "Configure columns")) self.actionSomething.setText(_translate("MainWindow", "something")) + self.action_send_animation.setText(_translate("MainWindow", "Animation")) diff --git a/Server/server_gui.ui b/Server/server_gui.ui index e00e6f4..f6c70c1 100644 --- a/Server/server_gui.ui +++ b/Server/server_gui.ui @@ -7,7 +7,7 @@ 0 0 1360 - 816 + 869 @@ -71,7 +71,7 @@ - Qt::AlignHCenter|Qt::AlignTop + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop Qt::AlignHCenter|Qt::AlignTop @@ -85,7 +85,7 @@ Start after - Qt::AlignCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter @@ -96,16 +96,6 @@ - - - - Qt::RightToLeft - - - Music after - - - @@ -119,16 +109,6 @@ - - - - Qt::RightToLeft - - - Play music - - - @@ -157,6 +137,26 @@ + + + + Qt::RightToLeft + + + Music after + + + + + + + Qt::RightToLeft + + + Play music + + + @@ -426,7 +426,7 @@ 0 0 1360 - 25 + 22 @@ -470,10 +470,12 @@ Send + + + + - - @@ -538,7 +540,7 @@ - Launch files + Launch files folder @@ -639,7 +641,7 @@ - FCU parameters + FCU parameters file @@ -707,6 +709,11 @@ something + + + Animation + + start_delay_spin diff --git a/Server/server_qt.py b/Server/server_qt.py index aae525b..ee61029 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -112,10 +112,6 @@ class MainWindow(QtWidgets.QMainWindow): self.init_model() - # self.statusBar = QStatusBar() - # self.setStatusBar(self.statusBar) - # self.statusBar.showMessage("Hey", 2000) - self.register_callbacks() self.player = QtMultimedia.QMediaPlayer() @@ -161,6 +157,7 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.action_send_animations.triggered.connect(self.send_animations) self.ui.action_send_calibrations.triggered.connect(self.send_calibrations) + self.ui.action_send_animation.triggered.connect(self.send_animation) self.ui.action_send_configurations.triggered.connect(self.send_config) self.ui.action_send_aruco_map.triggered.connect(self.send_aruco) self.ui.action_send_launch_file.triggered.connect(self.send_launch) @@ -171,7 +168,7 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.action_retrive_any_file.triggered.connect(b_partial(self.request_any_file, client_path=None)) self.ui.action_restart_clever.triggered.connect( - b_partial(self.send_to_selected, "service_restart", command_kwargs={"name": "clever"})) + b_partial(self.send_to_selected, "service_restart", command_kwargs={"name": "clover"})) self.ui.action_restart_clever_show.triggered.connect(self.restart_clever_show) self.ui.action_restart_chrony.triggered.connect(self.restart_chrony) self.ui.action_reboot_all.triggered.connect(b_partial(self.send_to_selected, "reboot_all")) @@ -291,7 +288,7 @@ class MainWindow(QtWidgets.QMainWindow): try: col = self.model.columns.index(key) except ValueError: - logging.error(f"No column {key} present!") + logging.debug(f"No column {key} present!") else: row_data = self.model.get_row_by_attr("client", client) row_num = self.model.get_row_index(row_data) @@ -481,6 +478,10 @@ class MainWindow(QtWidgets.QMainWindow): self.send_directory_files("Select directory with animations", ('.csv', '.txt'), match_id=True, client_path="", client_filename="animation.csv") + @pyqtSlot() + def send_animation(self): + self.send_files("Select animation file", "Animation files (*.csv)", onefile=True, client_filename="animation.csv") + @pyqtSlot() def send_calibrations(self): self.send_directory_files("Select directory with calibrations", ('.yaml', ), match_id=True, @@ -646,7 +647,7 @@ if __name__ == "__main__": msgbox_handler.setLevel(logging.CRITICAL) logging.basicConfig( - level=logging.DEBUG, + level=logging.INFO, format="%(asctime)s [%(name)-7.7s] [%(threadName)-19.19s] [%(levelname)-7.7s] %(message)s", handlers=[ logging.FileHandler("server_logs/{}.log".format(now)), diff --git a/blender-addon/addon.py b/blender-addon/addon.py index 221e6cf..e0cb41f 100644 --- a/blender-addon/addon.py +++ b/blender-addon/addon.py @@ -8,13 +8,13 @@ from bpy.types import Operator from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty bl_info = { - "name": "Export > CSV Drone Swarm Animation Exporter (.csv)", - "author": "Artem Vasiunik", - "version": (0, 4, 0), + "name": "clever-show animation (.csv)", + "author": "Artem Vasiunik & Arthur Golubtsov", + "version": (0, 5, 0), "blender": (2, 80, 0), #"api": 36079, - "location": "File > Export > CSV Drone Swarm Animation Exporter (.csv)", - "description": "Export > CSV Drone Swarm Animation Exporter (.csv)", + "location": "File > Export > clever-show animation (.csv)", + "description": "Export > clever-show animation (.csv)", "warning": "", "wiki_url": "https://github.com/CopterExpress/clever-show/blob/master/blender-addon/README.md", "tracker_url": "https://github.com/CopterExpress/clever-show/issues", @@ -23,20 +23,20 @@ bl_info = { class ExportCsv(Operator, ExportHelper): - bl_idname = "export_swarm_anim.folder" - bl_label = "Export Drone Swarm animation" + bl_idname = "export_animation.folder" + bl_label = "Export clever-show animation" filename_ext = '' use_filter_folder = True use_namefilter: bpy.props.BoolProperty( name="Use name filter for objects", - default=True, + default=False, ) drones_name: bpy.props.StringProperty( name="Name identifier", description="Name identifier for all drone objects", - default="copter" + default="clever" ) show_warnings: bpy.props.BoolProperty( @@ -61,7 +61,7 @@ class ExportCsv(Operator, ExportHelper): filepath: StringProperty( name="File Path", - description="File path used for exporting CSV files", + description="File path used for exporting csv files", maxlen=1024, subtype='DIR_PATH', default="" @@ -96,11 +96,11 @@ class ExportCsv(Operator, ExportHelper): distance_exeeded = False prev_x, prev_y, prev_z = 0, 0, 0 - + animation_file_writer.writerow([ os.path.splitext(bpy.path.basename(bpy.data.filepath))[0] ]) - + for frame_number in range(frame_start, frame_end + 1): scene.frame_set(frame_number) rgb = get_rgb_from_object(drone_obj) @@ -135,9 +135,7 @@ class ExportCsv(Operator, ExportHelper): round(rot_z, 5), *rgb, ]) - - - + if speed_exeeded: self.report({'WARNING'}, "Drone '%s' speed limits exeeded" % drone_obj.name) if distance_exeeded: @@ -195,7 +193,7 @@ def calc_distance(start_point, end_point): def menu_func(self, context): self.layout.operator( ExportCsv.bl_idname, - text="CSV Drone Swarm Animation Exporter (.csv)" + text="clever-show animation (.csv)" ) diff --git a/blender-addon/examples/basic.blend b/blender-addon/examples/basic.blend new file mode 100644 index 0000000..39d58b0 Binary files /dev/null and b/blender-addon/examples/basic.blend differ diff --git a/blender-addon/examples/basic/clever-1.csv b/blender-addon/examples/basic/clever-1.csv new file mode 100644 index 0000000..e7e37cb --- /dev/null +++ b/blender-addon/examples/basic/clever-1.csv @@ -0,0 +1,51 @@ +basic +1,0.0,0.0,0.0,0.0,204,2,0 +2,0.0,0.0,0.0,0.0,204,9,1 +3,0.0,0.0,0.0,0.0,204,21,2 +4,0.0,0.0,0.0,0.0,204,37,3 +5,0.0,0.0,0.0,0.0,204,56,4 +6,0.0,0.0,0.0,0.0,204,77,5 +7,0.0,0.0,0.0,0.0,204,97,6 +8,0.0,0.0,0.0,0.0,204,116,6 +9,0.0,0.0,0.0,0.0,204,131,7 +10,0.0,0.0,0.0,0.0,204,143,7 +11,0.0,0.0,0.1,0.0,199,153,7 +12,0.0,0.0,0.2,0.0,185,163,6 +13,0.0,0.0,0.3,0.0,163,172,6 +14,0.0,0.0,0.4,0.0,134,181,5 +15,0.0,0.0,0.5,0.0,102,188,5 +16,0.0,0.0,0.6,0.0,69,194,4 +17,0.0,0.0,0.7,0.0,40,199,3 +18,0.0,0.0,0.8,0.0,18,201,3 +19,0.0,0.0,0.9,0.0,4,203,2 +20,0.0,0.0,1.0,0.0,0,204,2 +21,0.02217,0.0,1.0,0.0,0,204,2 +22,0.08889,0.0,1.0,0.0,0,204,2 +23,0.19737,0.0,1.0,0.0,0,204,2 +24,0.33926,0.0,1.0,0.0,0,204,2 +25,0.5,0.0,1.0,0.0,0,204,2 +26,0.66074,0.0,1.0,0.0,0,204,2 +27,0.80263,0.0,1.0,0.0,0,204,2 +28,0.91111,0.0,1.0,0.0,0,204,2 +29,0.97783,0.0,1.0,0.0,0,204,2 +30,1.0,0.0,1.0,0.0,0,204,2 +31,1.0,0.0,0.9,0.0,3,204,2 +32,1.0,0.0,0.8,0.0,14,204,2 +33,1.0,0.0,0.7,0.0,32,204,2 +34,1.0,0.0,0.6,0.0,56,204,1 +35,1.0,0.0,0.5,0.0,84,204,1 +36,1.0,0.0,0.4,0.0,112,204,0 +37,1.0,0.0,0.3,0.0,138,204,0 +38,1.0,0.0,0.2,0.0,159,204,0 +39,1.0,0.0,0.1,0.0,175,204,0 +40,1.0,0.0,0.0,0.0,183,204,0 +41,1.0,0.0,0.0,0.0,188,199,0 +42,1.0,0.0,0.0,0.0,192,185,0 +43,1.0,0.0,0.0,0.0,196,163,0 +44,1.0,0.0,0.0,0.0,199,135,0 +45,1.0,0.0,0.0,0.0,201,102,0 +46,1.0,0.0,0.0,0.0,202,69,0 +47,1.0,0.0,0.0,0.0,203,40,0 +48,1.0,0.0,0.0,0.0,203,18,0 +49,1.0,0.0,0.0,0.0,203,5,0 +50,1.0,0.0,0.0,0.0,204,0,0 diff --git a/blender-addon/examples/two_drones_test.blend b/blender-addon/examples/two_drones_test.blend deleted file mode 100644 index 7a8d5c9..0000000 Binary files a/blender-addon/examples/two_drones_test.blend and /dev/null differ diff --git a/docs/ru/server.md b/docs/ru/server.md index 191e164..e6d7ac8 100644 --- a/docs/ru/server.md +++ b/docs/ru/server.md @@ -5,7 +5,7 @@ * [Установка и запуск](start-tutorial.md#установка-и-запуск-сервера) * [Интерфейс](#интерфейс-сервера) * [Настройка](#настройка-сервера) -* [Дополнительные операции](#дополнительные-операции) +* [Дополнительные операции](#дополнительные-операции-и-окна) ## Интерфейс сервера @@ -17,7 +17,7 @@ При первом подключении клиента к серверу в таблицу добавляется строка для отображения состояния клиента, содержащая только имя клиента (`copter ID`). Если на клиентах настроена автоматическая передача телеметрии, данные в таблице будут обновляться автоматически. Так же возможно запросить телеметрию выбранных клиентов с помощью кнопки [`Preflight check`](#управление). -Строки можно сортировать по возрастанию или убыванию значений любого из столбцов, кликнув по его заголовку. +Строки можно сортировать по возрастанию или убыванию значений любого из столбцов, кликнув по его заголовку. Столбцы можно менять местами и изменять их ширину: все изменения сохраняются в файле конфигурации сервера при штатном завершении работы сервера. При нажатии правой кнопкой мыши на шапку таблицы откроется контекстное меню с [встроенным конфигуратором](#column-preset-editor), в котором можно скрыть или отобразить столбцы, изменить их порядок, загрузить определенный набор настроек. При запуске сервера последние использованные настройки будут загружены и применены. @@ -34,7 +34,7 @@ #### Столбцы таблицы * `copter ID` - имя клиента. Может быть сконфигурирован на стороне клиента. Отображается сразу при подключении клиента. Рядом с каждым ID коптера расположен чекбокс - коптеры, чей ID отмечен чекбоксом положительно (галочка), считаются *выбранными*. Ячейки в этом столбце всегда проходят проверку. - * При двойном нажатии на это поле можно ввести новый `copter ID` клиента и переименовать его. В качестве имени допустимы сочетания латинских букв, цифр и тире (A-Z, a-z, 0-9, '-') длинной не более 63 символов. Тире не может являться первым символом. + * При двойном нажатии на это поле можно ввести новый `copter ID` клиента и переименовать его. В качестве имени допустимы сочетания латинских букв, цифр и тире (A-Z, a-z, 0-9, '-') длинной не более 63 символов. Тире не может являться первым символом. * `version` - хеш-код текущей git версии клиента. Ячейки в этом столбце проверяются при включенном (значение `true`) параметре [check_git_version](#раздел-checks), задаваемом в настройках сервера. Ячейка в данном столбце проходит проверку если хеш-код git версии данного клиента и сервера совпадают (если сервер не расположен в git-репозитории, то проверка проходится автоматически). * `configuration` - заданная пользователем версия конфигурации клиента. Ячейки в этом столбце всегда проходят проверку. * Ячейки этого столбца поддерживают *drag-and-drop*. При перетаскивании ячейки в любое стороннее приложение, поддерживающее файлы (к примеру, "Проводник"), файл конфигурации клиента будет скопирован в указанное место. При перетаскивании ячейки на другую ячейку файл конфигурации будет скопирован с одного на другой. При перетаскивании файла на ячейку он будет записан на клиент в качестве конфигурации (при условии валидации). При передаче конфигурации на клиент секция `PRIVATE` не будет отправляться. @@ -45,7 +45,7 @@ * `mode` - режим полётного контроллера. Ячейка в данном столбце не проходит проверку, если её значение `NO_FCU` или содержит `CMODE`. В остальных случаях, если ячейка не пустая, она проходит проверку. * `checks` - состояние самодиагностики коптера. Ячейка в данном столбце проходит проверку, если её значение `OK`. В остальных случаях, если ячейка не пустая, она не проходит проверку. * При двойном клике на ячейку при наличии ошибок будет показано диалоговое окно с полной детализацией всех ошибок. -* `current x y z yaw frame_id` - текущее положение коптера с указанием названия системы координат. Ячейка автоматически проходит проверку если у параметра [check_current_position ](#раздел-checks) установлено значение `false`. Иначе, ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или содержит `nan`. В остальных случаях, если ячейка не пустая, она проходит проверку. +* `current x y z yaw frame_id` - текущее положение коптера с указанием названия системы координат. Ячейка автоматически проходит проверку если у параметра [check_current_position](#раздел-checks) установлено значение `false`. Иначе, ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или содержит `nan`. В остальных случаях, если ячейка не пустая, она проходит проверку. * `start x y z` - стартовое положение коптера для воспроизведения анимации. Ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или разница между текущим и стартовым положением коптера больше значения [start_pos_delta_max](#раздел-checks). В остальных случаях, если ячейка не пустая, она проходит проверку. * `dt` - разница между временем на сервере и клиенте в секундах, включая сетевую задержку. Ячейка в данном столбце проходит проверку, если её значение меньше значения [time_delta_max](#раздел-checks), задаваемого в настройках сервера. В остальных случаях, если ячейка не пустая, она не проходит проверку. При слишком больших значениях сигнализирует об отсутствии синхронизации времени между коптером и клиентом. @@ -136,7 +136,7 @@ #### Управление -Данный раздел команд предназначен для выскоуровневого управления роем дронов. +Данный раздел команд предназначен для высокоуровневого управления роем дронов. * Спинбокс `Start after` - задаёт время задержки синхронного запуска выполнения анимаций коптерами после нажатия на кнопку `Start animation`. Для загруженных, подверженных помехам или имеющих большой пинг сетей рекомендуется использовать значения больше нуля. * Спинбокс `Music after` - задаёт время задержки запуска музыки после нажатия на кнопку `Start animation`. @@ -145,7 +145,7 @@ * Кнопка `Start animation` - посылает время старта анимации на все выбранные коптеры с учётом заданного в спинбоксе `Start after` времени. Все выбранные коптеры начинают синхронное воспроизведение анимации после нажатия на данную кнопку и через время, заданное в спинбоксе `Start after`. По окончанию анимации все коптеры выполнят посадку на месте окончания своей анимации. Кнопка активна только в том случае, если все коптеры готовы к воспроизведению анимации. При нажатии запрашивается дополнительное предупреждение. * Кнопка `Pause/Resume` - ставит на паузу и возобновляет выполнение полётных задач. После каждого нажатия кнопка меняет состояние на обратное. * Состояние`Pause` - ставит на паузу очередь заданий всех выбранных коптеров: приостанавливается выполнение любого полётного задания. Рекомендуется использовать в чрезвычайных ситуациях для определения неисправного коптера. **Внимание!** Данная команда НЕ прерывает полёт коптера в уже указанную точку (например: элементы взлёта, посадки; следование до начальной точки анимации и т.д.) - * Состояние `Resume` - все выбранные коптеры *синхронизированно* продолжат выполнение своих очередей заданий (например исполнение анимации) + * Состояние `Resume` - все выбранные коптеры *синхронизировано* продолжат выполнение своих очередей заданий (например исполнение анимации) #### Средства перехвата в экстренных ситуациях @@ -180,7 +180,7 @@ ### Файл конфигурации -Конфигурация сервера создаётся согласно [спецификации](../../Server/config/spec/configspec_server.ini), в ней можно посмотреть значения по умолчанию для любого параметра после ключевого слова `default`. Все изменения сохраняются в файл конфигурации `server.ini` в папке `clever-show/Server/config`. +Конфигурация сервера создаётся согласно [спецификации](../../Server/config/spec/configspec_server.ini), в ней можно посмотреть значения по умолчанию для любого параметра после ключевого слова `default`. Все изменения сохраняются в файл конфигурации `server.ini` в папке `clever-show/Server/config`. Доступно редактирование конфигурации сервера через GUI модуль `Config editor` через меню `Server -> Edit server config`. @@ -218,7 +218,7 @@ * `check_git_version` - Будет ли производиться проверка соответствия git-версий клиента и сервера для индикации в ячейках столбца `version` * `check_current_position` - Будет ли производиться проверка корректности текущих координат коптера для индикации в ячейках столбца `current x y z yaw frame_id`. -* `battery_percentage_min` - Минимальный заряд батарии коптера, допустимый для взлёта. Указывается *в процентах* (дробное значение от 0 до 100). Значение меньше указанного будет отмечено в столбце `battery` как неудовлетворительное. +* `battery_percentage_min` - Минимальный заряд батареи коптера, допустимый для взлёта. Указывается *в процентах* (дробное значение от 0 до 100). Значение меньше указанного будет отмечено в столбце `battery` как неудовлетворительное. * `start_pos_delta_max` - Максимальное расстояние от текущего положения коптера до его точки взлёта в файле анимации, допустимое для взлёта. Указывается *в метрах* (дробное значение от 0 до 'inf'). Значение больше указанного будет отмечено в столбце `start x y z` как неудовлетворительное. Допустимо использование строки 'inf' для любого допустимого расстояния. * `time_delta_max` - Максимальная разница (абсолютное значение) между временем сервера и клиента (включая сетевую задержку), допустимая для взлёта. Указывается *в секундах* (дробное значение от 0 до 'inf'). Значение больше указанного будет отмечено в столбце `dt` как неудовлетворительное. @@ -227,7 +227,7 @@ Сервер может использовать UDP broadcast, чтобы передавать клиентам актуальную информацию о конфигурации сервера. Таким образом становится возможным автоматическое подключение клиентов к серверу без необходимости дополнительной ручной конфигурации. В данном разделе задаются параметры этого механизма: * `send` - будут ли использованы broadcast'ы для передачи данных (при значении `False` broadcast'ы НЕ будут отправляться). Используйте `False` в случае повышенных требований безопасности, перегруженности сети или невозможности передачи по широковещательному каналу (из-за конфигурации брандмауэра или сети) -* `listen` - будет ли сервер прослушивать порт бродкастов для автоматического выключения во избежание наличия нескольких серверов в одной сети. +* `listen` - будет ли сервер прослушивать порт бродкастов для автоматического выключения во избежание наличия нескольких серверов в одной сети. * `port` - UDP порт, по которому будет осуществляться отправка сообщений. *Рекомендуется изменить значение по умолчанию в целях безопасности.* **Внимание!** При изменении этого параметра клиенты НЕ смогут принимать сообщения автоконфигурации до изменения (вручную) соответствующего параметра в конфигурации клиента на равное значение. * `delay` - периодичность (в секундах, дробное значение), с которой будет происходить отправка broadcast сообщений. Увеличьте задержку для уменьшения нагрузки на сеть. Уменьшите задержку для уменьшения времени отклика и подключения при первом запуске клиентов. @@ -249,59 +249,57 @@ ![LED Visual Land](../assets/server-led-emergency-land.png) -При нажатии на кнопку `Visual land` все коптеры делятся на 2 равные группы по порядку расположения в таблице. Первая половина коптеров зажигает светодиодную ленту зелёным цветом, вторая - красным. При нажатии на зелёную или красную кнопку происходит выбор группы, соответствующей цвету нажатой кнопки. Коптеры выбранного цвета снова делятся на две половины и каждая половина зажигает светодиодную ленту зелёным и красным цветом соответственно. Остальные коптеры выключают светодиодную ленту. +При нажатии на кнопку `Visual land` все коптеры делятся на 2 равные группы по порядку расположения в таблице. Первая половина коптеров зажигает светодиодную ленту зелёным цветом, вторая - красным. При нажатии на зелёную или красную кнопку происходит выбор группы, соответствующей цвету нажатой кнопки. Коптеры выбранного цвета снова делятся на две половины, и каждая половина зажигает светодиодную ленту зелёным и красным цветом соответственно. Остальные коптеры выключают светодиодную ленту. -Нажимая на кнопки, соответствующие цвету группы, в которой находится неисправный коптер, можно определить его номер и выполнить экстренную посадку за логарифмическое количество шагов от количества коптеров, т.е. гораздо быстрее, чем перебирая коптеры по одному. +Нажимая на кнопки, соответствующие цвету группы, в которой находится неисправный коптер, можно определить его номер и выполнить экстренную посадку за логарифмическое количество шагов от количества коптеров, т. е. гораздо быстрее, чем перебирая коптеры по одному. На любом шаге можно произвести посадку или выключение моторов всех коптеров, на которых включена светодиодная лента, нажав кнопку `Land` или `Disarm`. ### Config editor -Встроенный редактор конфигураций позволяет открывать и интуитивно редактировать файлы конфигурации сервера и клиента, ровно как и сторонние файлы конфигурации формата `.ini`. Редактор может быть запущен с помощью контекстного меню в таблице коптеров в сервере, из меню сервера, или запущен как отдельное приложение. +Встроенный редактор конфигураций позволяет открывать и интуитивно редактировать файлы конфигурации сервера и клиента, равно как и сторонние файлы конфигурации формата `.ini`. Редактор может быть запущен с помощью контекстного меню в таблице коптеров в сервере, из меню сервера, или запущен как отдельное приложение. -Значение каждого столбца может быть отредактировано по двойному клику на него. +Значение каждого столбца может быть отредактировано по двойному клику на него. -Значения столбца `Option` (названия секций и параметров) не могут повторяться в одном и том же уровне древа - названия секций и параметров будут автоматически изменятся с добавлением нумерации при повторяющихся наименованиях. +Значения столбца `Option` (названия секций и параметров) не могут повторяться в одном и том же уровне древа - названия секций и параметров будут автоматически изменяться с добавлением нумерации при повторяющихся наименованиях. Для значений столбца `Value` (фактические значения параметров) будет использован соответствующий встроенный редактор значений (для целочисленных, дробных и булевых значений). Значения массивов любых типов также отображается в виде элементов древа. Каждое из этих значений можно редактировать, перемещать, удалять и добавлять независимо. -При редактировании параметра без значения можно указать значение ` ` или в виде списка в виде `[1, 2, 3]` или `(1, 2, 3)` - это автоматически преобразует значение в список. +При редактировании параметра без значения можно указать значение `` или в виде списка в виде `[1, 2, 3]` или `(1, 2, 3)` - это автоматически преобразует значение в список. -![Config editor](..\assets\server-config-editor.png) +![Config editor](../assets/server-config-editor.png) Файл конфигурации отображается в редакторе в виде древа. Каждая "строка" в представлении - раздел или параметр конфигурации. Разделы конфигурации могут быть свёрнуты. Каждая строка может быть перемещена с помощью drag-n-drop для изменения порядка параметров и структуры разделов (возможно перетаскивание строки *на* раздел конфигурации для перемещения другого раздела\параметра в него). При зажатой клавише `alt` при перетаскивании параметр или раздел будут скопированы. -- Чекбокс `Color Indication` Включает или отключает цветовую индикацию состояния строк: - - *Синий* - Данного параметра *не было* в файле конфигурации при загрузке и его значение было взято из спецификации конфигурации. Изменение данного параметра приведет к добавлению его в текущий файл конфигурации. - - *Бирюзовый* - Данный параметр находится в файле конфигурации, но его значение совпадает со значением по умолчанию из спецификации конфигурации. - - *Жёлтый* - Значение данного параметра было изменено пользователем в текущей сессии редактирования. - - *Зелёный* - Данный параметр был добавлен в текущей сессии редактирования. - - *Красный* - Данный параметр был исключен из конфигурации пользователем текущей сессии редактирования. Он не будет сохранен в конфигурации. -- Чекбокс `Restart` - доступен только при редактировании конфигураций сервера или клиента. Автоматически перезапускает сервер или клиент после сохранения конфигурации. -- Кнопка `Save as` - Позволяет сохранить копию файла конфигурации на компьютер. В диалоговом окне выберите расположение и имя файла. **Внимание!** Конфигурация *не будет* проверена на соответствие спецификации! -- Кнопка `Save` - Сохраняет файл конфигурации. Перед сохранением будет проведена проверка файла на соответствие текущей конфигурации спецификации. При возникновении ошибок будет выведен детальный отчет по ним и будет предложено продолжить редактирование. При сохранении конфигурации сгенерированной на основе спецификации будет предложено выбрать новый путь для сохранения файла. -- Кнопка `Cancel` - Отменяет внесенные в конфигурацию изменения и закрывает диалоговое окно редактора конфигураций. - - +* Чекбокс `Color Indication` Включает или отключает цветовую индикацию состояния строк: + * *Синий* - Данного параметра *не было* в файле конфигурации при загрузке и его значение было взято из спецификации конфигурации. Изменение данного параметра приведет к добавлению его в текущий файл конфигурации. + * *Бирюзовый* - Данный параметр находится в файле конфигурации, но его значение совпадает со значением по умолчанию из спецификации конфигурации. + * *Жёлтый* - Значение данного параметра было изменено пользователем в текущей сессии редактирования. + * *Зелёный* - Данный параметр был добавлен в текущей сессии редактирования. + * *Красный* - Данный параметр был исключен из конфигурации пользователем текущей сессии редактирования. Он не будет сохранен в конфигурации. +* Чекбокс `Restart` - доступен только при редактировании конфигураций сервера или клиента. Автоматически перезапускает сервер или клиент после сохранения конфигурации. +* Кнопка `Save as` - Позволяет сохранить копию файла конфигурации на компьютер. В диалоговом окне выберите расположение и имя файла. **Внимание!** Конфигурация *не будет* проверена на соответствие спецификации! +* Кнопка `Save` - Сохраняет файл конфигурации. Перед сохранением будет проведена проверка файла на соответствие текущей конфигурации спецификации. При возникновении ошибок будет выведен детальный отчет по ним и будет предложено продолжить редактирование. При сохранении конфигурации, сгенерированной на основе спецификации будет предложено выбрать новый путь для сохранения файла. +* Кнопка `Cancel` - Отменяет внесенные в конфигурацию изменения и закрывает диалоговое окно редактора конфигураций. #### Контекстное меню и команды редактора конфигураций ![Cconfig editor context menu](../assets/server-config-editor-menu.png) -- `Duplicate` (`Shift+D`) - создает копию параметра или раздела (со всему входящими в него параметрами). К имени опции или раздела будет добавлена нумерация для избегания повторяющихся названий. -- `Toggle exclude` (`Alt+Del`) - исключает параметр или раздел из конфигурации. Параметр или раздел *не* будут удалены, но *не* будут записаны при сохранении. Повторное применение этой команды на уже исключенных параметрах или разделах вернет их к нормальному состоянию. -- `Remove from config` (`Del`) - *удаляет* параметр или раздел из конфигурации. **Внимание!** Это действие необратимо! -- `Clear item value` (`Shift+R`) - *удаляет* значение параметра или всех параметров раздела (если использовано на разделе). Это так же приводит к сбросу типа значения параметра и он будет восстановлен из следующего текстового ввода при редактировании значения. **Внимание!** Это действие необратимо! -- `Reset value to default` (`Ctrl+R`) - приводит к значению по умолчанию (из спецификации конфигурации) значение параметра или всех параметров раздела (если использовано на разделе). Это так же приводит к сбросу типа значения параметра и он будет восстановлен из следующего текстового ввода при редактировании значения. **Внимание!** Это действие необратимо! -- `Reset all changes` (`Alt+R`) - восстанавливает к первоначальному виду параметр или все параметры раздела (если использовано на разделе). **Внимание!** Это действие необратимо! -- `Add option` (`Shift+A`) - добавляет параметр с пустым значением, первое редактирование значения определит его тип -- `Add section`(`Ctrl+A`) - добавляет раздел. +* `Duplicate` (`Shift+D`) - создает копию параметра или раздела (со всему входящими в него параметрами). К имени опции или раздела будет добавлена нумерация для избегания повторяющихся названий. +* `Toggle exclude` (`Alt+Del`) - исключает параметр или раздел из конфигурации. Параметр или раздел *не* будут удалены, но *не* будут записаны при сохранении. Повторное применение этой команды на уже исключенных параметрах или разделах вернет их к нормальному состоянию. +* `Remove from config` (`Del`) - *удаляет* параметр или раздел из конфигурации. **Внимание!** Это действие необратимо! +* `Clear item value` (`Shift+R`) - *удаляет* значение параметра или всех параметров раздела (если использовано на разделе). Это так же приводит к сбросу типа значения параметра, и он будет восстановлен из следующего текстового ввода при редактировании значения. **Внимание!** Это действие необратимо! +* `Reset value to default` (`Ctrl+R`) - приводит к значению по умолчанию (из спецификации конфигурации) значение параметра или всех параметров раздела (если использовано на разделе). Это так же приводит к сбросу типа значения параметра, и он будет восстановлен из следующего текстового ввода при редактировании значения. **Внимание!** Это действие необратимо! +* `Reset all changes` (`Alt+R`) - восстанавливает к первоначальному виду параметр или все параметры раздела (если использовано на разделе). **Внимание!** Это действие необратимо! +* `Add option` (`Shift+A`) - добавляет параметр с пустым значением, первое редактирование значения определит его тип. +* `Add section`(`Ctrl+A`) - добавляет раздел. ### Column preset editor Позволяет редактировать, удалять и добавлять наборы настроек отображения столбцов в таблице. -Окно редактора столбцов таблицы имеет два вида: +Окно редактора столбцов таблицы имеет два вида: #### Контекстное меню @@ -309,10 +307,9 @@ Может быть открыто с помощью нажатия правой кнопкой мыши на шапку таблицы. - -- `Список столбцов` - Нажатием на чекбоксы (флажки) можно скрывать или отображать столбцы таблицы. Перетаскиванием строк (drag-and-drop) можно изменять порядок столбцов. Изменения будут применены моментально и автоматически сохранены в конфигурацию сервера. -- `Выпадающий список наборов настроек` - Позволяет загрузить и автоматически применить любой из записанных в файле конфигурации наборов настроек. -- Кнопка `Manage presets` - открывает диалоговое окно для редактирования наборов настроек (см. далее) +* `Список столбцов` - Нажатием на чекбоксы (флажки) можно скрывать или отображать столбцы таблицы. Перетаскиванием строк (drag-and-drop) можно изменять порядок столбцов. Изменения будут применены моментально и автоматически сохранены в конфигурацию сервера. +* `Выпадающий список наборов настроек` - Позволяет загрузить и автоматически применить любой из записанных в файле конфигурации наборов настроек. +* Кнопка `Manage presets` - открывает диалоговое окно для редактирования наборов настроек (см. далее). #### Диалоговое окно @@ -320,10 +317,10 @@ Может быть открыто из контекстного меню или меню сервера в разделе `Table`. -- `Список столбцов` - Нажатием на чекбоксы (флажки) можно скрывать или отображать столбцы таблицы. Перетаскиванием строк (drag-and-drop) можно изменять порядок столбцов. *Изменения будут применены или сохранены только после нажатия кнопок `Save` или `Apply` .* -- `Выпадающий список наборов настроек` - Позволяет выбрать для редактирования любой из записанных в файле конфигурации наборов настроек. - - Пункт `` - Создает новый набор настроек. В появившемся диалоговом окне введите имя набора настроек. Новый набор настроек будет основываться на наборе по умолчанию (`DEFAULT`). -- Кнопка `Add` - Создает новый набор настроек. В появившемся диалоговом окне введите имя набора настроек. Новый набор настроек будет основываться на наборе по умолчанию (`DEFAULT`). -- Кнопка `Remove` - Удаляет выбранный набор настроек. **Внимание!** Это действие необратимо! -- Кнопка `Save` - Сохраняет набор настроек в файл конфигурации. -- Кнопка `Apply` - Сохраняет набор настроек в файл конфигурации и применяет его к таблице. \ No newline at end of file +* `Список столбцов` - Нажатием на чекбоксы (флажки) можно скрывать или отображать столбцы таблицы. Перетаскиванием строк (drag-and-drop) можно изменять порядок столбцов. *Изменения будут применены или сохранены только после нажатия кнопок `Save` или `Apply` .* +* `Выпадающий список наборов настроек` - Позволяет выбрать для редактирования любой из записанных в файле конфигурации наборов настроек. + * Пункт `` - Создает новый набор настроек. В появившемся диалоговом окне введите имя набора настроек. Новый набор настроек будет основываться на наборе по умолчанию (`DEFAULT`). +* Кнопка `Add` - Создает новый набор настроек. В появившемся диалоговом окне введите имя набора настроек. Новый набор настроек будет основываться на наборе по умолчанию (`DEFAULT`). +* Кнопка `Remove` - Удаляет выбранный набор настроек. **Внимание!** Это действие необратимо! +* Кнопка `Save` - Сохраняет набор настроек в файл конфигурации. +* Кнопка `Apply` - Сохраняет набор настроек в файл конфигурации и применяет его к таблице. diff --git a/tests/animation_1.csv b/tests/animation_1.csv new file mode 100644 index 0000000..e7e37cb --- /dev/null +++ b/tests/animation_1.csv @@ -0,0 +1,51 @@ +basic +1,0.0,0.0,0.0,0.0,204,2,0 +2,0.0,0.0,0.0,0.0,204,9,1 +3,0.0,0.0,0.0,0.0,204,21,2 +4,0.0,0.0,0.0,0.0,204,37,3 +5,0.0,0.0,0.0,0.0,204,56,4 +6,0.0,0.0,0.0,0.0,204,77,5 +7,0.0,0.0,0.0,0.0,204,97,6 +8,0.0,0.0,0.0,0.0,204,116,6 +9,0.0,0.0,0.0,0.0,204,131,7 +10,0.0,0.0,0.0,0.0,204,143,7 +11,0.0,0.0,0.1,0.0,199,153,7 +12,0.0,0.0,0.2,0.0,185,163,6 +13,0.0,0.0,0.3,0.0,163,172,6 +14,0.0,0.0,0.4,0.0,134,181,5 +15,0.0,0.0,0.5,0.0,102,188,5 +16,0.0,0.0,0.6,0.0,69,194,4 +17,0.0,0.0,0.7,0.0,40,199,3 +18,0.0,0.0,0.8,0.0,18,201,3 +19,0.0,0.0,0.9,0.0,4,203,2 +20,0.0,0.0,1.0,0.0,0,204,2 +21,0.02217,0.0,1.0,0.0,0,204,2 +22,0.08889,0.0,1.0,0.0,0,204,2 +23,0.19737,0.0,1.0,0.0,0,204,2 +24,0.33926,0.0,1.0,0.0,0,204,2 +25,0.5,0.0,1.0,0.0,0,204,2 +26,0.66074,0.0,1.0,0.0,0,204,2 +27,0.80263,0.0,1.0,0.0,0,204,2 +28,0.91111,0.0,1.0,0.0,0,204,2 +29,0.97783,0.0,1.0,0.0,0,204,2 +30,1.0,0.0,1.0,0.0,0,204,2 +31,1.0,0.0,0.9,0.0,3,204,2 +32,1.0,0.0,0.8,0.0,14,204,2 +33,1.0,0.0,0.7,0.0,32,204,2 +34,1.0,0.0,0.6,0.0,56,204,1 +35,1.0,0.0,0.5,0.0,84,204,1 +36,1.0,0.0,0.4,0.0,112,204,0 +37,1.0,0.0,0.3,0.0,138,204,0 +38,1.0,0.0,0.2,0.0,159,204,0 +39,1.0,0.0,0.1,0.0,175,204,0 +40,1.0,0.0,0.0,0.0,183,204,0 +41,1.0,0.0,0.0,0.0,188,199,0 +42,1.0,0.0,0.0,0.0,192,185,0 +43,1.0,0.0,0.0,0.0,196,163,0 +44,1.0,0.0,0.0,0.0,199,135,0 +45,1.0,0.0,0.0,0.0,201,102,0 +46,1.0,0.0,0.0,0.0,202,69,0 +47,1.0,0.0,0.0,0.0,203,40,0 +48,1.0,0.0,0.0,0.0,203,18,0 +49,1.0,0.0,0.0,0.0,203,5,0 +50,1.0,0.0,0.0,0.0,204,0,0 diff --git a/tests/animation_2.csv b/tests/animation_2.csv new file mode 100755 index 0000000..addc6f0 --- /dev/null +++ b/tests/animation_2.csv @@ -0,0 +1,1066 @@ +parad +0,-1.00519,2.65699,0.21,-2.76315,7,255,0 +1,-1.00519,2.65699,0.21,-2.76315,7,255,0 +2,-1.00519,2.65699,0.21,-2.76315,7,255,0 +3,-1.00519,2.65699,0.21,-2.76315,7,255,0 +4,-1.00519,2.65699,0.21,-2.76315,7,255,0 +5,-1.00519,2.65699,0.21,-2.76315,7,255,0 +6,-1.00519,2.65699,0.21,-2.76315,7,255,0 +7,-1.00519,2.65699,0.21,-2.76315,7,255,0 +8,-1.00519,2.65699,0.21,-2.76315,7,255,0 +9,-1.00519,2.65699,0.21,-2.76315,7,255,0 +10,-1.00519,2.65699,0.21,-2.76315,7,255,0 +11,-1.00519,2.65699,0.21,-2.76315,7,255,0 +12,-1.00519,2.65699,0.21,-2.76315,7,255,0 +13,-1.00519,2.65699,0.21,-2.76315,7,255,0 +14,-1.00519,2.65699,0.21,-2.76315,7,255,0 +15,-1.00519,2.65699,0.21,-2.76315,7,255,0 +16,-1.00519,2.65699,0.21,-2.76315,7,255,0 +17,-1.00519,2.65699,0.21,-2.76315,7,255,0 +18,-1.00519,2.65699,0.21,-2.76315,7,255,0 +19,-1.00519,2.65699,0.21,-2.76315,7,255,0 +20,-1.00519,2.65699,0.21,-2.76315,7,255,0 +21,-1.00519,2.65699,0.21,-2.76315,7,255,0 +22,-1.00519,2.65699,0.21,-2.76315,7,255,0 +23,-1.00519,2.65699,0.21,-2.76315,7,255,0 +24,-1.00519,2.65699,0.21,-2.76315,7,255,0 +25,-1.00519,2.65699,0.21,-2.76315,7,255,0 +26,-1.00519,2.65699,0.21,-2.76315,7,255,0 +27,-1.00519,2.65699,0.21,-2.76315,7,255,0 +28,-1.00519,2.65699,0.21,-2.76315,7,255,0 +29,-1.00519,2.65699,0.21,-2.76315,7,255,0 +30,-1.00519,2.65699,0.21,-2.76315,7,255,0 +31,-1.00519,2.65699,0.21,-2.76315,7,255,0 +32,-1.00519,2.65699,0.21,-2.76315,7,255,0 +33,-1.00519,2.65699,0.21,-2.76315,7,255,0 +34,-1.00519,2.65699,0.21,-2.76315,7,255,0 +35,-1.00519,2.65699,0.21,-2.76315,7,255,0 +36,-1.00519,2.65699,0.21,-2.76315,7,255,0 +37,-1.00519,2.65699,0.21,-2.76315,7,255,0 +38,-1.00519,2.65699,0.21,-2.76315,7,255,0 +39,-1.00519,2.65699,0.21,-2.76315,7,255,0 +40,-1.00519,2.65699,0.21,-2.76315,7,255,0 +41,-1.00519,2.65699,0.21,-2.76315,7,255,0 +42,-1.00519,2.65699,0.21,-2.76315,7,255,0 +43,-1.00519,2.65699,0.21,-2.76315,7,255,0 +44,-1.00519,2.65699,0.21,-2.76315,7,255,0 +45,-1.00519,2.65699,0.21,-2.76315,7,255,0 +46,-1.00519,2.65699,0.21,-2.76315,7,255,0 +47,-1.00519,2.65699,0.21,-2.76315,7,255,0 +48,-1.00519,2.65699,0.21,-2.76315,7,255,0 +49,-1.00519,2.65699,0.21,-2.76315,7,255,0 +50,-1.00519,2.65699,0.21,-2.76315,7,255,0 +51,-1.00519,2.65699,0.21,-2.76315,7,255,0 +52,-1.00519,2.65699,0.21,-2.76315,7,255,0 +53,-1.00519,2.65699,0.21,-2.76315,7,255,0 +54,-1.00519,2.65699,0.21,-2.76315,7,255,0 +55,-1.00519,2.65699,0.21,-2.76315,7,255,0 +56,-1.00519,2.65699,0.21,-2.76315,7,255,0 +57,-1.00519,2.65699,0.21,-2.76315,7,255,0 +58,-1.00519,2.65699,0.21,-2.76315,7,255,0 +59,-1.00519,2.65699,0.21,-2.76315,7,255,0 +60,-1.00519,2.65699,0.21,-2.76315,7,255,0 +61,-1.00519,2.65699,0.21,-2.76315,7,255,0 +62,-1.00519,2.65699,0.21,-2.76315,7,255,0 +63,-1.00519,2.65699,0.21,-2.76315,7,255,0 +64,-1.00519,2.65699,0.21,-2.76315,7,255,0 +65,-1.00519,2.65699,0.21,-2.76315,7,255,0 +66,-1.00519,2.65699,0.21,-2.76315,7,255,0 +67,-1.00519,2.65699,0.21,-2.76315,7,255,0 +68,-1.00519,2.65699,0.21,-2.76315,7,255,0 +69,-1.00519,2.65699,0.21,-2.76315,7,255,0 +70,-1.00519,2.65699,0.21,-2.76315,7,255,0 +71,-1.00519,2.65699,0.21,-2.76315,7,255,0 +72,-1.00519,2.65699,0.21,-2.76315,7,255,0 +73,-1.00519,2.65699,0.21,-2.76315,7,255,0 +74,-1.00519,2.65699,0.21,-2.76315,7,255,0 +75,-1.00519,2.65699,0.21,-2.76315,7,255,0 +76,-1.00519,2.65699,0.21,-2.76315,7,255,0 +77,-1.00519,2.65699,0.21,-2.76315,7,255,0 +78,-1.00519,2.65699,0.21,-2.76315,7,255,0 +79,-1.00519,2.65699,0.21,-2.76315,7,255,0 +80,-1.00519,2.65699,0.21,-2.76315,7,255,0 +81,-1.00519,2.65699,0.21,-2.76315,7,255,0 +82,-1.00519,2.65699,0.21,-2.76315,7,255,0 +83,-1.00519,2.65699,0.21,-2.76315,7,255,0 +84,-1.00519,2.65699,0.21,-2.76315,7,255,0 +85,-1.00519,2.65699,0.21,-2.76315,7,255,0 +86,-1.00519,2.65699,0.21,-2.76315,7,255,0 +87,-1.00519,2.65699,0.21,-2.76315,7,255,0 +88,-1.00519,2.65699,0.21,-2.76315,7,255,0 +89,-1.00519,2.65699,0.21,-2.76315,7,255,0 +90,-1.00519,2.65699,0.21,-2.76315,7,255,0 +91,-1.00519,2.65699,0.21,-2.76315,7,255,0 +92,-1.00519,2.65699,0.21,-2.76315,7,255,0 +93,-1.00519,2.65699,0.21,-2.76315,7,255,0 +94,-1.00519,2.65699,0.21,-2.76315,7,255,0 +95,-1.00519,2.65699,0.21,-2.76315,7,255,0 +96,-1.00519,2.65699,0.21,-2.76315,7,255,0 +97,-1.00519,2.65699,0.21,-2.76315,7,255,0 +98,-1.00519,2.65699,0.21,-2.76315,7,255,0 +99,-1.00519,2.65699,0.21,-2.76315,7,255,0 +100,-1.00519,2.65699,0.21,-2.76315,7,255,0 +101,-1.00519,2.65699,0.21,-2.76315,7,255,0 +102,-1.00519,2.65699,0.21,-2.76315,7,255,0 +103,-1.00519,2.65699,0.21,-2.76315,7,255,0 +104,-1.00519,2.65699,0.21,-2.76315,7,255,0 +105,-1.00519,2.65699,0.21,-2.76315,7,255,0 +106,-1.00519,2.65699,0.21,-2.76315,7,255,0 +107,-1.00519,2.65699,0.21,-2.76315,7,255,0 +108,-1.00519,2.65699,0.21,-2.76315,7,255,0 +109,-1.00519,2.65699,0.21,-2.76315,7,255,0 +110,-1.00519,2.65699,0.21,-2.76315,7,255,0 +111,-1.00519,2.65699,0.21,-2.76315,7,255,0 +112,-1.00519,2.65699,0.21,-2.76315,7,255,0 +113,-1.00519,2.65699,0.21,-2.76315,7,255,0 +114,-1.00519,2.65699,0.21,-2.76315,7,255,0 +115,-1.00519,2.65699,0.21,-2.76315,7,255,0 +116,-1.00519,2.65699,0.21,-2.76315,7,255,0 +117,-1.00519,2.65699,0.21,-2.76315,7,255,0 +118,-1.00519,2.65699,0.21,-2.76315,7,255,0 +119,-1.00519,2.65699,0.21,-2.76315,7,255,0 +120,-1.00519,2.65699,0.21,-2.76315,7,255,0 +121,-1.00519,2.65699,0.21,-2.76315,7,255,0 +122,-1.00519,2.65699,0.21,-2.76315,7,255,0 +123,-1.00519,2.65699,0.21,-2.76315,7,255,0 +124,-1.00519,2.65699,0.21,-2.76315,7,255,0 +125,-1.00519,2.65699,0.21,-2.76315,7,255,0 +126,-1.00519,2.65699,0.21,-2.76315,7,255,0 +127,-1.00519,2.65699,0.21,-2.76315,7,255,0 +128,-1.00519,2.65699,0.21,-2.76315,7,255,0 +129,-1.00519,2.65699,0.21,-2.76315,7,255,0 +130,-1.00519,2.65699,0.21,-2.76315,7,255,0 +131,-1.00519,2.65699,0.21,-2.76315,7,255,0 +132,-1.00519,2.65699,0.21,-2.76315,7,255,0 +133,-1.00519,2.65699,0.21,-2.76315,7,255,0 +134,-1.00519,2.65699,0.21,-2.76315,7,255,0 +135,-1.00519,2.65699,0.21,-2.76315,7,255,0 +136,-1.00519,2.65699,0.21,-2.76315,7,255,0 +137,-1.00519,2.65699,0.21,-2.76315,7,255,0 +138,-1.00519,2.65699,0.21,-2.76315,7,255,0 +139,-1.00519,2.65699,0.21,-2.76315,7,255,0 +140,-1.00519,2.65699,0.21,-2.76315,7,255,0 +141,-1.00519,2.65699,0.21,-2.76315,7,255,0 +142,-1.00519,2.65699,0.21,-2.76315,7,255,0 +143,-1.00519,2.65699,0.21,-2.76315,7,255,0 +144,-1.00519,2.65699,0.21,-2.76315,7,255,0 +145,-1.00519,2.65699,0.21,-2.76315,7,255,0 +146,-1.00519,2.65699,0.21,-2.76315,7,255,0 +147,-1.00519,2.65699,0.21,-2.76315,7,255,0 +148,-1.00519,2.65699,0.21,-2.76315,7,255,0 +149,-1.00519,2.65699,0.21,-2.76315,7,255,0 +150,-1.00519,2.65699,0.21,-2.76315,7,255,0 +151,-1.00519,2.65699,0.21,-2.76315,7,255,0 +152,-1.00519,2.65699,0.21,-2.76315,7,255,0 +153,-1.00519,2.65699,0.21,-2.76315,7,255,0 +154,-1.00519,2.65699,0.21,-2.76315,7,255,0 +155,-1.00519,2.65699,0.21,-2.76315,7,255,0 +156,-1.00519,2.65699,0.21,-2.76315,7,255,0 +157,-1.00519,2.65699,0.21,-2.76315,7,255,0 +158,-1.00519,2.65699,0.21,-2.76315,7,255,0 +159,-1.00519,2.65699,0.21,-2.76315,7,255,0 +160,-1.00519,2.65699,0.21,-2.76315,7,255,0 +161,-1.00519,2.65699,0.21,-2.76315,7,255,0 +162,-1.00519,2.65699,0.21,-2.76315,7,255,0 +163,-1.00519,2.65699,0.21,-2.76315,7,255,0 +164,-1.00519,2.65699,0.21,-2.76315,7,255,0 +165,-1.00519,2.65699,0.21,-2.76315,7,255,0 +166,-1.00519,2.65699,0.21,-2.76315,7,255,0 +167,-1.00519,2.65699,0.21,-2.76315,7,255,0 +168,-1.00519,2.65699,0.21,-2.76315,7,255,0 +169,-1.00519,2.65699,0.21,-2.76315,7,255,0 +170,-1.00519,2.65699,0.21,-2.76315,7,255,0 +171,-1.00519,2.65699,0.21,-2.76315,7,255,0 +172,-1.00519,2.65699,0.21,-2.76315,7,255,0 +173,-1.00519,2.65699,0.21,-2.76315,7,255,0 +174,-1.00519,2.65699,0.21,-2.76315,7,255,0 +175,-1.00519,2.65699,0.21,-2.76315,7,255,0 +176,-1.00519,2.65699,0.21,-2.76315,7,255,0 +177,-1.00519,2.65699,0.21,-2.76315,7,255,0 +178,-1.00519,2.65699,0.21,-2.76315,7,255,0 +179,-1.00519,2.65699,0.21,-2.76315,7,255,0 +180,-1.00519,2.65699,0.21,-2.76315,7,255,0 +181,-1.00519,2.65699,0.21,-2.76315,7,255,0 +182,-1.00519,2.65699,0.21,-2.76315,7,255,0 +183,-1.00519,2.65699,0.21,-2.76315,7,255,0 +184,-1.00519,2.65699,0.21,-2.76315,7,255,0 +185,-1.00519,2.65699,0.21,-2.76315,7,255,0 +186,-1.00519,2.65699,0.21,-2.76315,7,255,0 +187,-1.00519,2.65699,0.21,-2.76315,7,255,0 +188,-1.00519,2.65699,0.21,-2.76315,7,255,0 +189,-1.00519,2.65699,0.21,-2.76315,7,255,0 +190,-1.00519,2.65699,0.21,-2.76315,7,255,0 +191,-1.00519,2.65699,0.21,-2.76315,7,255,0 +192,-1.00519,2.65699,0.21,-2.76315,7,255,0 +193,-1.00519,2.65699,0.21,-2.76315,7,255,0 +194,-1.00519,2.65699,0.21,-2.76315,7,255,0 +195,-1.00519,2.65699,0.21,-2.76315,7,255,0 +196,-1.00519,2.65699,0.21,-2.76315,7,255,0 +197,-1.00519,2.65699,0.21,-2.76315,7,255,0 +198,-1.00519,2.65699,0.21,-2.76315,7,255,0 +199,-1.00519,2.65699,0.21,-2.76315,7,255,0 +200,-1.00519,2.65699,0.21,-2.76315,7,255,0 +201,-1.00519,2.65699,0.21,-2.76315,7,255,0 +202,-1.00519,2.65699,0.21,-2.76315,7,255,0 +203,-1.00519,2.65699,0.21,-2.76315,7,255,0 +204,-1.00519,2.65699,0.21,-2.76315,7,255,0 +205,-1.00519,2.65699,0.21,-2.76315,7,255,0 +206,-1.00519,2.65699,0.21,-2.76315,7,255,0 +207,-1.00519,2.65699,0.21,-2.76315,7,255,0 +208,-1.00519,2.65699,0.21,-2.76315,7,255,0 +209,-1.00519,2.65699,0.21,-2.76315,7,255,0 +210,-1.00519,2.65699,0.21,-2.76315,7,255,0 +211,-1.00519,2.65699,0.21,-2.76315,7,255,0 +212,-1.00519,2.65699,0.21,-2.76315,7,255,0 +213,-1.00519,2.65699,0.21,-2.76315,7,255,0 +214,-1.00519,2.65699,0.21,-2.76315,7,255,0 +215,-1.00519,2.65699,0.21,-2.76315,7,255,0 +216,-1.00519,2.65699,0.21,-2.76315,7,255,0 +217,-1.00519,2.65699,0.21,-2.76315,7,255,0 +218,-1.00519,2.65699,0.21,-2.76315,7,255,0 +219,-1.00519,2.65699,0.21,-2.76315,7,255,0 +220,-1.00519,2.65699,0.21,-2.76315,7,255,0 +221,-1.00519,2.65699,0.21,-2.76315,7,255,0 +222,-1.00519,2.65699,0.21,-2.76315,7,255,0 +223,-1.00519,2.65699,0.21,-2.76315,7,255,0 +224,-1.00519,2.65699,0.21,-2.76315,7,255,0 +225,-1.00519,2.65699,0.21,-2.76315,7,255,0 +226,-1.00519,2.65699,0.21,-2.76315,7,255,0 +227,-1.00519,2.65699,0.21,-2.76315,7,255,0 +228,-1.00519,2.65699,0.21,-2.76315,7,255,0 +229,-1.00519,2.65699,0.21,-2.76315,7,255,0 +230,-1.00519,2.65699,0.21,-2.76315,7,255,0 +231,-1.00519,2.65699,0.21,-2.76315,7,255,0 +232,-1.00519,2.65699,0.21,-2.76315,7,255,0 +233,-1.00519,2.65699,0.21,-2.76315,7,255,0 +234,-1.00519,2.65699,0.21,-2.76315,7,255,0 +235,-1.00519,2.65699,0.21,-2.76315,7,255,0 +236,-1.00519,2.65699,0.21,-2.76315,7,255,0 +237,-1.00519,2.65699,0.21,-2.76315,7,255,0 +238,-1.00519,2.65699,0.21,-2.76315,7,255,0 +239,-1.00519,2.65699,0.21,-2.76315,7,255,0 +240,-1.00519,2.65699,0.21,-2.76315,7,255,0 +241,-1.00519,2.65699,0.21,-2.76315,7,255,0 +242,-1.00519,2.65699,0.21,-2.76315,7,255,0 +243,-1.00519,2.65699,0.21,-2.76315,7,255,0 +244,-1.00519,2.65699,0.21,-2.76315,7,255,0 +245,-1.00519,2.65699,0.21,-2.76315,7,255,0 +246,-1.00519,2.65699,0.21,-2.76315,7,255,0 +247,-1.00519,2.65699,0.21,-2.76315,7,255,0 +248,-1.00519,2.65699,0.21,-2.76315,7,255,0 +249,-1.00519,2.65699,0.21,-2.76315,7,255,0 +250,-1.00519,2.65699,0.21,-2.76315,7,255,0 +251,-1.00519,2.65699,0.21,-2.76315,7,255,0 +252,-1.00519,2.65699,0.21,-2.76315,7,255,0 +253,-1.00519,2.65699,0.21,-2.76315,7,255,0 +254,-1.00519,2.65699,0.21,-2.76315,7,255,0 +255,-1.00519,2.65699,0.21,-2.76315,7,255,0 +256,-1.00519,2.65699,0.21,-2.76315,7,255,0 +257,-1.00519,2.65699,0.21,-2.76315,7,255,0 +258,-1.00519,2.65699,0.21,-2.76315,7,255,0 +259,-1.00519,2.65699,0.21,-2.76315,7,255,0 +260,-1.00519,2.65699,0.21,-2.76315,7,255,0 +261,-1.00519,2.65699,0.21,-2.76315,7,255,0 +262,-1.00519,2.65699,0.21,-2.76315,7,255,0 +263,-1.00519,2.65699,0.21,-2.76315,7,255,0 +264,-1.00519,2.65699,0.21,-2.76315,7,255,0 +265,-1.00519,2.65699,0.21,-2.76315,7,255,0 +266,-1.00519,2.65699,0.21,-2.76315,7,255,0 +267,-1.00519,2.65699,0.21,-2.76315,7,255,0 +268,-1.00519,2.65699,0.21,-2.76315,7,255,0 +269,-1.00519,2.65699,0.21,-2.76315,7,255,0 +270,-1.00519,2.65699,0.21,-2.76315,7,255,0 +271,-1.00519,2.65699,0.24386,-2.76315,7,255,0 +272,-1.00519,2.65699,0.30495,-2.76315,7,255,0 +273,-1.00519,2.65699,0.36315,-2.76315,7,255,0 +274,-1.00519,2.65699,0.41951,-2.76315,7,255,0 +275,-1.00519,2.65699,0.47568,-2.76315,7,255,0 +276,-1.00519,2.65699,0.53229,-2.76315,7,255,0 +277,-1.00519,2.65699,0.58898,-2.76315,7,255,0 +278,-1.00519,2.65699,0.64513,-2.76315,7,255,0 +279,-1.00519,2.65699,0.70179,-2.76315,7,255,0 +280,-1.00519,2.65699,0.75979,-2.76315,7,255,0 +281,-1.00519,2.65699,0.81297,-2.76315,7,255,0 +282,-1.00519,2.65699,0.86881,-2.76315,7,255,0 +283,-1.00519,2.65699,0.93843,-2.76315,7,255,0 +284,-1.00453,2.65334,0.9872,-2.76315,7,255,0 +285,-1.00118,2.61466,1.0,-2.76315,7,255,0 +286,-0.99839,2.52817,1.0,-2.76315,7,255,0 +287,-0.99588,2.44199,1.0,-2.76315,7,255,0 +288,-0.99316,2.37257,1.0,-2.76315,7,255,0 +289,-0.99048,2.297,1.0,-2.76315,7,255,0 +290,-0.98782,2.21961,1.0,-2.76315,7,255,0 +291,-0.98515,2.14444,1.0,-2.76315,7,255,0 +292,-0.98247,2.06873,1.0,-2.76315,7,255,0 +293,-0.9798,1.9928,1.0,-2.76315,7,255,0 +294,-0.97713,1.91706,1.0,-2.76315,7,255,0 +295,-0.97446,1.84129,1.0,-2.76315,7,255,0 +296,-0.97178,1.7655,1.0,-2.76315,7,255,0 +297,-0.96911,1.68972,1.0,-2.76315,7,255,0 +298,-0.96644,1.61394,1.0,-2.76315,7,255,0 +299,-0.96377,1.53816,1.0,-2.76315,7,255,0 +300,-0.96109,1.46238,1.0,-2.76315,7,255,0 +301,-0.95842,1.3866,1.0,-2.76315,7,255,0 +302,-0.95575,1.31083,1.0,-2.76315,7,255,0 +303,-0.95308,1.23504,1.0,-2.76315,7,255,0 +304,-0.9504,1.15926,1.0,-2.76315,7,255,0 +305,-0.94773,1.08349,1.0,-2.76315,7,255,0 +306,-0.94506,1.00771,1.0,-2.76315,7,255,0 +307,-0.94239,0.93192,1.0,-2.76315,7,255,0 +308,-0.93971,0.85615,1.0,-2.76315,7,255,0 +309,-0.93704,0.78037,1.0,-2.76315,7,255,0 +310,-0.93437,0.70458,1.0,-2.76315,7,255,0 +311,-0.9317,0.62881,1.0,-2.76315,7,255,0 +312,-0.92902,0.55304,1.0,-2.76315,7,255,0 +313,-0.92636,0.47725,1.0,-2.76315,7,255,0 +314,-0.92366,0.40136,1.0,-2.76315,7,255,0 +315,-0.92101,0.32571,1.0,-2.76315,7,255,0 +316,-0.91843,0.2505,1.0,-2.76315,7,255,0 +317,-0.91542,0.17384,1.0,-2.76315,7,255,0 +318,-0.91299,0.09409,1.0,-2.76315,7,255,0 +319,-0.90963,0.02185,1.0,-2.76315,7,255,0 +320,-0.87383,0.00163,1.0,-2.76315,7,255,0 +321,-0.77046,0.0,1.0,-2.76315,7,255,0 +322,-0.69735,0.0,1.0,-2.76315,7,255,0 +323,-0.63239,0.0,1.0,-2.76315,7,255,0 +324,-0.55624,0.0,1.0,-2.76315,7,255,0 +325,-0.48324,0.0,1.0,-2.76315,7,255,0 +326,-0.41137,0.0,1.0,-2.76315,7,255,0 +327,-0.33828,0.0,1.0,-2.76315,7,255,0 +328,-0.26549,0.0,1.0,-2.76315,7,255,0 +329,-0.19282,0.0,1.0,-2.76315,7,255,0 +330,-0.12004,0.0,1.0,-2.76315,7,255,0 +331,-0.04728,0.0,1.0,-2.76315,7,255,0 +332,0.02546,0.0,1.0,-2.76315,7,255,0 +333,0.09822,0.0,1.0,-2.76315,7,255,0 +334,0.17097,0.0,1.0,-2.76315,7,255,0 +335,0.24373,0.0,1.0,-2.76315,7,255,0 +336,0.31648,0.0,1.0,-2.76315,7,255,0 +337,0.38924,0.0,1.0,-2.76315,7,255,0 +338,0.46199,0.0,1.0,-2.76315,7,255,0 +339,0.53475,0.0,1.0,-2.76315,7,255,0 +340,0.6075,0.0,1.0,-2.76315,7,255,0 +341,0.68025,0.0,1.0,-2.76315,7,255,0 +342,0.75301,0.0,1.0,-2.76315,7,255,0 +343,0.82576,0.0,1.0,-2.76315,7,255,0 +344,0.89852,0.0,1.0,-2.76315,7,255,0 +345,0.97127,0.0,1.0,-2.76315,7,255,0 +346,1.04403,0.0,1.0,-2.76315,7,255,0 +347,1.11678,0.0,1.0,-2.76315,7,255,0 +348,1.18953,0.0,1.0,-2.76315,7,255,0 +349,1.26229,0.0,1.0,-2.76315,7,255,0 +350,1.33504,0.0,1.0,-2.76315,7,255,0 +351,1.4078,0.0,1.0,-2.76315,7,255,0 +352,1.48055,0.0,1.0,-2.76315,7,255,0 +353,1.55331,0.0,1.0,-2.76315,7,255,0 +354,1.62606,0.0,1.0,-2.76315,7,255,0 +355,1.69881,0.0,1.0,-2.76315,7,255,0 +356,1.77157,0.0,1.0,-2.76315,7,255,0 +357,1.84432,0.0,1.0,-2.76315,7,255,0 +358,1.91707,0.0,1.0,-2.76315,7,255,0 +359,1.98983,0.0,1.0,-2.76315,7,255,0 +360,2.06258,0.0,1.0,-2.76315,7,255,0 +361,2.13534,0.0,1.0,-2.76315,7,255,0 +362,2.20809,0.0,1.0,-2.76315,7,255,0 +363,2.28084,0.0,1.0,-2.76315,7,255,0 +364,2.3536,0.0,1.0,-2.76315,7,255,0 +365,2.42635,0.0,1.0,-2.76315,7,255,0 +366,2.49911,0.0,1.0,-2.76315,7,255,0 +367,2.57186,0.0,1.0,-2.76315,7,255,0 +368,2.64461,0.0,1.0,-2.76315,7,255,0 +369,2.71737,0.0,1.0,-2.76315,7,255,0 +370,2.79012,0.0,1.0,-2.76315,7,255,0 +371,2.86288,0.0,1.0,-2.76315,7,255,0 +372,2.93563,0.0,1.0,-2.76315,7,255,0 +373,3.00839,0.0,1.0,-2.76315,7,255,0 +374,3.08114,0.0,1.0,-2.76315,7,255,0 +375,3.15389,0.0,1.0,-2.76315,7,255,0 +376,3.22665,0.0,1.0,-2.76315,7,255,0 +377,3.2994,0.0,1.0,-2.76315,7,255,0 +378,3.37216,0.0,1.0,-2.76315,7,255,0 +379,3.44492,0.0,1.0,-2.76315,7,255,0 +380,3.51765,0.0,1.0,-2.76315,7,255,0 +381,3.59042,0.0,1.0,-2.76315,7,255,0 +382,3.66323,0.0,1.0,-2.76315,7,255,0 +383,3.73581,0.0,1.0,-2.76315,7,255,0 +384,3.80863,0.0,1.0,-2.76315,7,255,0 +385,3.88208,0.0,1.0,-2.76315,7,255,0 +386,3.95309,0.0,1.0,-2.76315,7,255,0 +387,4.02606,0.0,1.0,-2.76315,7,255,0 +388,4.10654,-0.02619,1.03815,-2.766,7,255,0 +389,4.17222,-0.07,1.10188,-2.76964,7,255,0 +390,4.21934,-0.10777,1.15672,-2.77183,7,255,0 +391,4.26563,-0.144,1.20926,-2.77475,7,255,0 +392,4.31231,-0.18027,1.26178,-2.77798,7,255,0 +393,4.3582,-0.21679,1.31456,-2.78112,7,255,0 +394,4.40386,-0.25346,1.36747,-2.78438,7,255,0 +395,4.44921,-0.29032,1.42055,-2.78777,7,255,0 +396,4.49413,-0.3274,1.47382,-2.79127,7,255,0 +397,4.53865,-0.36469,1.52728,-2.7949,7,255,0 +398,4.5828,-0.40218,1.58092,-2.79865,7,255,0 +399,4.62655,-0.43987,1.63474,-2.80252,7,255,0 +400,4.66991,-0.47776,1.68872,-2.80652,7,255,0 +401,4.71287,-0.51586,1.74289,-2.81066,7,255,0 +402,4.75539,-0.55417,1.79725,-2.81494,7,255,0 +403,4.79746,-0.59271,1.8518,-2.81938,7,255,0 +404,4.83905,-0.63148,1.90655,-2.82397,7,255,0 +405,4.88015,-0.67049,1.9615,-2.82875,7,255,0 +406,4.92073,-0.70975,2.01666,-2.83371,7,255,0 +407,4.96076,-0.74927,2.07204,-2.83887,7,255,0 +408,5.00021,-0.78905,2.12764,-2.84425,7,255,0 +409,5.03906,-0.82911,2.18347,-2.84985,7,255,0 +410,5.07727,-0.86945,2.23954,-2.8557,7,255,0 +411,5.11481,-0.91009,2.29584,-2.86182,7,255,0 +412,5.15163,-0.95103,2.35239,-2.86822,7,255,0 +413,5.18772,-0.99229,2.40919,-2.87493,7,255,0 +414,5.22301,-1.03387,2.46625,-2.88196,7,255,0 +415,5.25747,-1.07579,2.52357,-2.88936,7,255,0 +416,5.29105,-1.11805,2.58116,-2.89714,7,255,0 +417,5.32371,-1.16067,2.63901,-2.90533,7,255,0 +418,5.35538,-1.20365,2.69714,-2.91397,7,255,0 +419,5.38603,-1.24701,2.75554,-2.9231,7,255,0 +420,5.41557,-1.29075,2.81421,-2.93275,7,255,0 +421,5.44396,-1.33489,2.87316,-2.94297,7,255,0 +422,5.47112,-1.37943,2.93238,-2.95382,7,255,0 +423,5.49698,-1.42437,2.99186,-2.96534,7,255,0 +424,5.52148,-1.46973,3.05161,-2.97757,7,255,0 +425,5.54449,-1.51552,3.11161,-2.99065,7,255,0 +426,5.56591,-1.56175,3.17188,-3.00459,7,255,0 +427,5.58555,-1.60846,3.23241,-3.0195,7,255,0 +428,5.60301,-1.65558,3.29306,-3.03606,7,255,0 +429,5.61879,-1.70323,3.35405,-3.05318,7,255,0 +430,5.63399,-1.75332,3.41792,-3.07028,7,255,0 +431,5.64777,-1.81252,3.49266,-3.0953,7,255,0 +432,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +433,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +434,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +435,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +436,5.65455,-1.84833,3.53759,-3.11273,7,255,0 +437,5.65455,-1.79891,3.53759,-3.11273,7,255,0 +438,5.65455,-1.72513,3.53759,-3.11273,7,255,0 +439,5.65455,-1.66957,3.53759,-3.11273,7,255,0 +440,5.65455,-1.61072,3.53759,-3.11273,7,255,0 +441,5.65455,-1.55011,3.53759,-3.11273,7,255,0 +442,5.65455,-1.49082,3.53759,-3.11273,7,255,0 +443,5.65456,-1.43129,3.53759,-3.11273,7,255,0 +444,5.65456,-1.37163,3.53759,-3.11273,7,255,0 +445,5.65456,-1.31206,3.53759,-3.11273,7,255,0 +446,5.65456,-1.25248,3.53759,-3.11273,7,255,0 +447,5.65456,-1.19289,3.53759,-3.11273,7,255,0 +448,5.65456,-1.13331,3.53759,-3.11273,7,255,0 +449,5.65456,-1.07373,3.53758,-3.11273,7,255,0 +450,5.65456,-1.01414,3.53758,-3.11273,7,255,0 +451,5.65456,-0.95456,3.53758,-3.11273,7,255,0 +452,5.65456,-0.89497,3.53758,-3.11273,7,255,0 +453,5.65456,-0.83539,3.53758,-3.11273,7,255,0 +454,5.65456,-0.7758,3.53758,-3.11273,7,255,0 +455,5.65456,-0.71622,3.53758,-3.11273,7,255,0 +456,5.65456,-0.65663,3.53758,-3.11273,7,255,0 +457,5.65456,-0.59705,3.53758,-3.11273,7,255,0 +458,5.65456,-0.53746,3.53758,-3.11273,7,255,0 +459,5.65456,-0.47788,3.53758,-3.11273,7,255,0 +460,5.65456,-0.41829,3.53758,-3.11273,7,255,0 +461,5.65456,-0.35871,3.53758,-3.11273,7,255,0 +462,5.65456,-0.29912,3.53758,-3.11273,7,255,0 +463,5.65456,-0.23954,3.53758,-3.11273,7,255,0 +464,5.65456,-0.17996,3.53758,-3.11273,7,255,0 +465,5.65456,-0.12037,3.53758,-3.11273,7,255,0 +466,5.65456,-0.06079,3.53758,-3.11273,7,255,0 +467,5.65456,-0.0012,3.53758,-3.11273,7,255,0 +468,5.65456,0.05838,3.53758,-3.11273,7,255,0 +469,5.65456,0.11797,3.53758,-3.11273,7,255,0 +470,5.65456,0.17755,3.53758,-3.11273,7,255,0 +471,5.65456,0.23714,3.53758,-3.11273,7,255,0 +472,5.65456,0.29672,3.53758,-3.11273,7,255,0 +473,5.65456,0.35631,3.53758,-3.11273,7,255,0 +474,5.65456,0.41589,3.53758,-3.11273,7,255,0 +475,5.65456,0.47548,3.53758,-3.11273,7,255,0 +476,5.65456,0.53506,3.53757,-3.11273,7,255,0 +477,5.65456,0.59465,3.53757,-3.11273,7,255,0 +478,5.65457,0.65423,3.53757,-3.11273,7,255,0 +479,5.65457,0.71381,3.53757,-3.11273,7,255,0 +480,5.65457,0.7734,3.53757,-3.11273,7,255,0 +481,5.65457,0.83298,3.53757,-3.11273,7,255,0 +482,5.65457,0.89257,3.53757,-3.11273,7,255,0 +483,5.65457,0.95215,3.53757,-3.11273,7,255,0 +484,5.65457,1.01174,3.53757,-3.11273,7,255,0 +485,5.65457,1.07132,3.53757,-3.11273,7,255,0 +486,5.65457,1.13091,3.53757,-3.11273,7,255,0 +487,5.65457,1.19049,3.53756,-3.11273,7,255,0 +488,5.65457,1.25008,3.53756,-3.11273,7,255,0 +489,5.65458,1.30966,3.53756,-3.11273,7,255,0 +490,5.65458,1.36925,3.53756,-3.11273,7,255,0 +491,5.65458,1.42883,3.53756,-3.11273,7,255,0 +492,5.65458,1.48841,3.53756,-3.11273,7,255,0 +493,5.65458,1.548,3.53756,-3.11273,7,255,0 +494,5.65458,1.60764,3.53756,-3.11273,7,255,0 +495,5.65459,1.66702,3.53755,-3.11486,7,255,0 +496,5.65574,1.72639,3.53755,3.11304,7,255,0 +497,5.66228,1.78358,3.53755,2.83459,7,255,0 +498,5.6874,1.82462,3.53755,2.39285,7,255,0 +499,5.73331,1.84438,3.53755,1.98516,7,255,0 +500,5.78976,1.85001,3.53755,1.7291,7,255,0 +501,5.84936,1.85028,3.53754,1.61928,7,255,0 +502,5.9089,1.85028,3.53754,1.59963,7,255,0 +503,5.96848,1.85028,3.53754,1.59962,7,255,0 +504,6.02807,1.85027,3.53754,1.59962,7,255,0 +505,6.08765,1.85027,3.53754,1.59962,7,255,0 +506,6.14723,1.85026,3.53753,1.59962,7,255,0 +507,6.20682,1.85025,3.53753,1.59962,7,255,0 +508,6.26641,1.85024,3.53753,1.59962,7,255,0 +509,6.32599,1.85023,3.53753,1.59961,7,255,0 +510,6.38558,1.85022,3.53753,1.59961,7,255,0 +511,6.44516,1.85021,3.53752,1.59961,7,255,0 +512,6.50474,1.8502,3.53752,1.59961,7,255,0 +513,6.56433,1.85019,3.53752,1.59961,7,255,0 +514,6.62391,1.85018,3.53752,1.59961,7,255,0 +515,6.6835,1.85017,3.53751,1.59961,7,255,0 +516,6.74308,1.85017,3.53751,1.59961,7,255,0 +517,6.80267,1.85016,3.53751,1.59961,7,255,0 +518,6.86225,1.85015,3.53751,1.5996,7,255,0 +519,6.92184,1.85014,3.5375,1.5996,7,255,0 +520,6.98142,1.85013,3.5375,1.5996,7,255,0 +521,7.04101,1.85013,3.5375,1.5996,7,255,0 +522,7.10059,1.85012,3.53749,1.5996,7,255,0 +523,7.16018,1.85011,3.53749,1.5996,7,255,0 +524,7.21976,1.8501,3.53749,1.5996,7,255,0 +525,7.27935,1.8501,3.53748,1.5996,7,255,0 +526,7.33893,1.85009,3.53748,1.5996,7,255,0 +527,7.39851,1.85008,3.53748,1.5996,7,255,0 +528,7.4581,1.85007,3.53747,1.5996,7,255,0 +529,7.51768,1.85007,3.53747,1.5996,7,255,0 +530,7.57727,1.85006,3.53747,1.5996,7,255,0 +531,7.63685,1.85005,3.53746,1.5996,7,255,0 +532,7.69644,1.85004,3.53746,1.5996,7,255,0 +533,7.75602,1.85004,3.53746,1.59959,7,255,0 +534,7.81561,1.85003,3.53745,1.59959,7,255,0 +535,7.87519,1.85002,3.53745,1.59959,7,255,0 +536,7.93478,1.85001,3.53745,1.59959,7,255,0 +537,7.99436,1.85,3.53744,1.59959,7,255,0 +538,8.05395,1.85,3.53744,1.59959,7,255,0 +539,8.11353,1.84999,3.53743,1.59959,7,255,0 +540,8.17312,1.84998,3.53743,1.59959,7,255,0 +541,8.2327,1.84997,3.53743,1.59959,7,255,0 +542,8.29228,1.84996,3.53742,1.59959,7,255,0 +543,8.35187,1.84996,3.53742,1.59959,7,255,0 +544,8.41145,1.84995,3.53741,1.59959,7,255,0 +545,8.47104,1.84994,3.53741,1.59959,7,255,0 +546,8.53063,1.84993,3.53741,1.59959,7,255,0 +547,8.59021,1.84992,3.5374,1.59959,7,255,0 +548,8.64979,1.84992,3.5374,1.59959,7,255,0 +549,8.70938,1.84991,3.5374,1.59959,7,255,0 +550,8.76896,1.8499,3.53739,1.59959,7,255,0 +551,8.82855,1.84989,3.53739,1.59959,7,255,0 +552,8.88813,1.84988,3.53738,1.59959,7,255,0 +553,8.94772,1.84988,3.53738,1.59959,7,255,0 +554,9.0073,1.84987,3.53738,1.59959,7,255,0 +555,9.06689,1.84986,3.53737,1.59959,7,255,0 +556,9.12647,1.84985,3.53737,1.59959,7,255,0 +557,9.18606,1.84985,3.53737,1.59959,7,255,0 +558,9.24564,1.84984,3.53736,1.59959,7,255,0 +559,9.30522,1.84983,3.53736,1.59959,7,255,0 +560,9.36481,1.84982,3.53735,1.59959,7,255,0 +561,9.42439,1.84981,3.53735,1.59959,7,255,0 +562,9.48398,1.84981,3.53735,1.59959,7,255,0 +563,9.54356,1.8498,3.53734,1.59959,7,255,0 +564,9.60315,1.84979,3.53734,1.59959,7,255,0 +565,9.66273,1.84978,3.53734,1.59959,7,255,0 +566,9.72232,1.84978,3.53733,1.59959,7,255,0 +567,9.7819,1.84977,3.53733,1.59959,7,255,0 +568,9.84149,1.84976,3.53733,1.59959,7,255,0 +569,9.90107,1.84975,3.53732,1.59959,7,255,0 +570,9.96066,1.84974,3.53732,1.59959,7,255,0 +571,10.02024,1.84974,3.53731,1.59959,7,255,0 +572,10.07983,1.84973,3.53731,1.59959,7,255,0 +573,10.13941,1.84972,3.53731,1.59959,7,255,0 +574,10.199,1.84971,3.5373,1.59959,7,255,0 +575,10.25858,1.8497,3.5373,1.59959,7,255,0 +576,10.31816,1.8497,3.53729,1.59959,7,255,0 +577,10.37775,1.84969,3.53729,1.59959,7,255,0 +578,10.43733,1.84968,3.53729,1.59959,7,255,0 +579,10.49692,1.84967,3.53728,1.59959,7,255,0 +580,10.5565,1.84967,3.53728,1.59959,7,255,0 +581,10.61609,1.84966,3.53727,1.59959,7,255,0 +582,10.67567,1.84965,3.53727,1.59959,7,255,0 +583,10.73526,1.84964,3.53727,1.59959,7,255,0 +584,10.79484,1.84963,3.53726,1.59959,7,255,0 +585,10.85443,1.84963,3.53726,1.59959,7,255,0 +586,10.91401,1.84962,3.53725,1.59959,7,255,0 +587,10.9736,1.84961,3.53725,1.59959,7,255,0 +588,11.03318,1.8496,3.53724,1.59959,7,255,0 +589,11.09277,1.8496,3.53724,1.59959,7,255,0 +590,11.15235,1.84959,3.53724,1.59959,7,255,0 +591,11.21194,1.84958,3.53723,1.59959,7,255,0 +592,11.27152,1.84957,3.53723,1.59959,7,255,0 +593,11.33111,1.84956,3.53723,1.59958,7,255,0 +594,11.39069,1.84956,3.53722,1.59958,7,255,0 +595,11.45028,1.84955,3.53722,1.59958,7,255,0 +596,11.50986,1.84954,3.53722,1.59958,7,255,0 +597,11.56944,1.84953,3.53721,1.59958,7,255,0 +598,11.62903,1.84952,3.53721,1.59958,7,255,0 +599,11.68862,1.84952,3.53721,1.59958,7,255,0 +600,11.7482,1.84951,3.53721,1.59958,7,255,0 +601,11.80779,1.8495,3.5372,1.59958,7,255,0 +602,11.86737,1.84949,3.5372,1.59958,7,255,0 +603,11.92696,1.84948,3.5372,1.59958,7,255,0 +604,11.98654,1.84947,3.5372,1.59957,7,255,0 +605,12.04613,1.84946,3.5372,1.59957,7,255,0 +606,12.10571,1.84946,3.5372,1.59957,7,255,0 +607,12.16529,1.84945,3.53719,1.59957,7,255,0 +608,12.22488,1.84944,3.53719,1.59957,7,255,0 +609,12.28446,1.84944,3.53719,1.59957,7,255,0 +610,12.34405,1.84943,3.53719,1.59957,7,255,0 +611,12.40363,1.84943,3.53719,1.59957,7,255,0 +612,12.46322,1.84943,3.53719,1.59956,7,255,0 +613,12.5228,1.84942,3.53719,1.59956,7,255,0 +614,12.58239,1.84942,3.53719,1.59956,7,255,0 +615,12.64197,1.84942,3.53719,1.59956,7,255,0 +616,12.70156,1.84941,3.53719,1.59956,7,255,0 +617,12.76113,1.8494,3.53718,1.59956,7,255,0 +618,12.82075,1.84938,3.53718,1.59952,7,255,0 +619,12.88031,1.84936,3.53718,1.59853,7,255,0 +620,12.93977,1.84932,3.53718,1.57955,7,255,0 +621,12.99975,1.8492,3.53718,1.53822,7,255,0 +622,13.05807,1.84683,3.53718,1.39467,7,255,0 +623,13.10874,1.8321,3.53718,1.02962,7,255,0 +624,13.14285,1.79679,3.53718,0.55984,7,255,0 +625,13.15555,1.74517,3.53718,0.18196,7,255,0 +626,13.15648,1.6865,3.53718,0.04504,7,255,0 +627,13.15648,1.62648,3.53718,0.0302,7,255,0 +628,13.15648,1.56702,3.53718,0.02887,7,255,0 +629,13.15648,1.50746,3.53717,0.02886,7,255,0 +630,13.15648,1.44785,3.53717,0.02886,7,255,0 +631,13.15648,1.38827,3.53717,0.02886,7,255,0 +632,13.15648,1.32869,3.53717,0.02886,7,255,0 +633,13.15648,1.2691,3.53717,0.02885,7,255,0 +634,13.15648,1.20952,3.53717,0.02885,7,255,0 +635,13.15648,1.14993,3.53717,0.02885,7,255,0 +636,13.15649,1.09035,3.53717,0.02885,7,255,0 +637,13.15648,1.03076,3.53717,0.02885,7,255,0 +638,13.15649,0.97118,3.53717,0.02885,7,255,0 +639,13.15649,0.9116,3.53717,0.02884,7,255,0 +640,13.15649,0.85201,3.53717,0.02884,7,255,0 +641,13.15649,0.79243,3.53717,0.02884,7,255,0 +642,13.15649,0.73284,3.53716,0.02884,7,255,0 +643,13.15649,0.67326,3.53716,0.02884,7,255,0 +644,13.15649,0.61367,3.53716,0.02884,7,255,0 +645,13.15649,0.55409,3.53716,0.02884,7,255,0 +646,13.15649,0.4945,3.53716,0.02884,7,255,0 +647,13.15649,0.43492,3.53716,0.02883,7,255,0 +648,13.15649,0.37533,3.53716,0.02883,7,255,0 +649,13.1565,0.31575,3.53716,0.02883,7,255,0 +650,13.1565,0.25616,3.53716,0.02883,7,255,0 +651,13.1565,0.19658,3.53716,0.02883,7,255,0 +652,13.1565,0.137,3.53716,0.02883,7,255,0 +653,13.1565,0.07741,3.53716,0.02883,7,255,0 +654,13.1565,0.01783,3.53716,0.02883,7,255,0 +655,13.1565,-0.04176,3.53716,0.02883,7,255,0 +656,13.1565,-0.10134,3.53716,0.02883,7,255,0 +657,13.1565,-0.16093,3.53715,0.02883,7,255,0 +658,13.1565,-0.22051,3.53715,0.02883,7,255,0 +659,13.1565,-0.2801,3.53715,0.02883,7,255,0 +660,13.1565,-0.33968,3.53715,0.02883,7,255,0 +661,13.15651,-0.39926,3.53715,0.02883,7,255,0 +662,13.15651,-0.45885,3.53715,0.02883,7,255,0 +663,13.15651,-0.51843,3.53715,0.02883,7,255,0 +664,13.15651,-0.57802,3.53715,0.02883,7,255,0 +665,13.15651,-0.6376,3.53715,0.02883,7,255,0 +666,13.15651,-0.69719,3.53715,0.02883,7,255,0 +667,13.15651,-0.75677,3.53715,0.02882,7,255,0 +668,13.15651,-0.81636,3.53715,0.02882,7,255,0 +669,13.15651,-0.87594,3.53715,0.02882,7,255,0 +670,13.15651,-0.93553,3.53715,0.02882,7,255,0 +671,13.15651,-0.99511,3.53715,0.02882,7,255,0 +672,13.15651,-1.05469,3.53715,0.02882,7,255,0 +673,13.15651,-1.11428,3.53715,0.02882,7,255,0 +674,13.15652,-1.17386,3.53715,0.02882,7,255,0 +675,13.15652,-1.23344,3.53714,0.02882,7,255,0 +676,13.15652,-1.29303,3.53714,0.02882,7,255,0 +677,13.15652,-1.35262,3.53714,0.02882,7,255,0 +678,13.15652,-1.41219,3.53714,0.02882,7,255,0 +679,13.15652,-1.47179,3.53714,0.02882,7,255,0 +680,13.15652,-1.53141,3.53714,0.02882,7,255,0 +681,13.15652,-1.59085,3.53714,0.02882,7,255,0 +682,13.15652,-1.65054,3.53714,0.02774,7,255,0 +683,13.15644,-1.71064,3.53714,-0.01199,7,255,0 +684,13.15416,-1.76717,3.53714,-0.20779,7,255,0 +685,13.13611,-1.81192,3.53714,-0.58791,7,255,0 +686,13.09665,-1.83671,3.53714,-1.05263,7,255,0 +687,13.04295,-1.84623,3.53714,-1.37746,7,255,0 +688,12.98346,-1.84808,3.53714,-1.50019,7,255,0 +689,12.9238,-1.84831,3.53714,-1.53617,7,255,0 +690,12.86434,-1.84831,3.53714,-1.54141,7,255,0 +691,12.80472,-1.84831,3.53714,-1.54193,7,255,0 +692,12.74513,-1.84831,3.53714,-1.54193,7,255,0 +693,12.68556,-1.84831,3.53714,-1.54193,7,255,0 +694,12.62597,-1.84831,3.53714,-1.54193,7,255,0 +695,12.56638,-1.84831,3.53714,-1.54193,7,255,0 +696,12.5068,-1.84831,3.53714,-1.54193,7,255,0 +697,12.44722,-1.84831,3.53714,-1.54193,7,255,0 +698,12.38763,-1.84831,3.53713,-1.54193,7,255,0 +699,12.32805,-1.84831,3.53713,-1.54193,7,255,0 +700,12.26846,-1.84831,3.53713,-1.54193,7,255,0 +701,12.20887,-1.84831,3.53713,-1.54193,7,255,0 +702,12.14929,-1.84831,3.53713,-1.54193,7,255,0 +703,12.08971,-1.84831,3.53713,-1.54193,7,255,0 +704,12.03012,-1.84831,3.53713,-1.54193,7,255,0 +705,11.97054,-1.84831,3.53713,-1.54193,7,255,0 +706,11.91095,-1.84831,3.53713,-1.54193,7,255,0 +707,11.85137,-1.84831,3.53713,-1.54193,7,255,0 +708,11.79178,-1.84831,3.53713,-1.54193,7,255,0 +709,11.7322,-1.84831,3.53713,-1.54193,7,255,0 +710,11.67261,-1.84831,3.53713,-1.54193,7,255,0 +711,11.61303,-1.84831,3.53713,-1.54193,7,255,0 +712,11.55344,-1.84831,3.53713,-1.54193,7,255,0 +713,11.49385,-1.84831,3.53713,-1.54193,7,255,0 +714,11.43427,-1.84831,3.53713,-1.54193,7,255,0 +715,11.37469,-1.84831,3.53713,-1.54193,7,255,0 +716,11.3151,-1.84831,3.53713,-1.54193,7,255,0 +717,11.25552,-1.84831,3.53713,-1.54193,7,255,0 +718,11.19593,-1.84832,3.53713,-1.54193,7,255,0 +719,11.13635,-1.84832,3.53713,-1.54193,7,255,0 +720,11.07676,-1.84832,3.53713,-1.54193,7,255,0 +721,11.01718,-1.84832,3.53713,-1.54193,7,255,0 +722,10.95759,-1.84832,3.53713,-1.54193,7,255,0 +723,10.89801,-1.84832,3.53713,-1.54193,7,255,0 +724,10.83842,-1.84832,3.53713,-1.54193,7,255,0 +725,10.77884,-1.84832,3.53713,-1.54193,7,255,0 +726,10.71925,-1.84832,3.53713,-1.54193,7,255,0 +727,10.65967,-1.84832,3.53713,-1.54193,7,255,0 +728,10.60008,-1.84832,3.53713,-1.54193,7,255,0 +729,10.5405,-1.84832,3.53713,-1.54193,7,255,0 +730,10.48091,-1.84832,3.53713,-1.54193,7,255,0 +731,10.42133,-1.84832,3.53713,-1.54193,7,255,0 +732,10.36174,-1.84832,3.53712,-1.54193,7,255,0 +733,10.30216,-1.84832,3.53712,-1.54193,7,255,0 +734,10.24257,-1.84832,3.53712,-1.54193,7,255,0 +735,10.18299,-1.84832,3.53712,-1.54193,7,255,0 +736,10.1234,-1.84832,3.53712,-1.54193,7,255,0 +737,10.06382,-1.84832,3.53712,-1.54193,7,255,0 +738,10.00423,-1.84832,3.53712,-1.54193,7,255,0 +739,9.94465,-1.84832,3.53712,-1.54193,7,255,0 +740,9.88506,-1.84832,3.53712,-1.54193,7,255,0 +741,9.82548,-1.84832,3.53712,-1.54193,7,255,0 +742,9.76589,-1.84832,3.53712,-1.54193,7,255,0 +743,9.70631,-1.84832,3.53712,-1.54193,7,255,0 +744,9.64673,-1.84832,3.53712,-1.54193,7,255,0 +745,9.58714,-1.84832,3.53712,-1.54193,7,255,0 +746,9.52755,-1.84832,3.53712,-1.54193,7,255,0 +747,9.46797,-1.84832,3.53712,-1.54193,7,255,0 +748,9.40839,-1.84832,3.53712,-1.54193,7,255,0 +749,9.3488,-1.84832,3.53712,-1.54193,7,255,0 +750,9.28922,-1.84832,3.53712,-1.54193,7,255,0 +751,9.22963,-1.84832,3.53712,-1.54193,7,255,0 +752,9.17004,-1.84832,3.53712,-1.54193,7,255,0 +753,9.11046,-1.84832,3.53712,-1.54193,7,255,0 +754,9.05088,-1.84832,3.53712,-1.54193,7,255,0 +755,8.99129,-1.84832,3.53712,-1.54193,7,255,0 +756,8.93171,-1.84832,3.53712,-1.54193,7,255,0 +757,8.87212,-1.84832,3.53712,-1.54193,7,255,0 +758,8.81254,-1.84832,3.53712,-1.54193,7,255,0 +759,8.75295,-1.84832,3.53712,-1.54193,7,255,0 +760,8.69337,-1.84832,3.53712,-1.54193,7,255,0 +761,8.63378,-1.84832,3.53712,-1.54193,7,255,0 +762,8.5742,-1.84832,3.53712,-1.54193,7,255,0 +763,8.51461,-1.84832,3.53712,-1.54193,7,255,0 +764,8.45503,-1.84832,3.53712,-1.54193,7,255,0 +765,8.39544,-1.84832,3.53712,-1.54193,7,255,0 +766,8.33586,-1.84832,3.53712,-1.54193,7,255,0 +767,8.27627,-1.84832,3.53712,-1.54193,7,255,0 +768,8.21669,-1.84832,3.53712,-1.54193,7,255,0 +769,8.1571,-1.84832,3.53712,-1.54193,7,255,0 +770,8.09752,-1.84832,3.53712,-1.54193,7,255,0 +771,8.03793,-1.84832,3.53712,-1.54193,7,255,0 +772,7.97835,-1.84832,3.53712,-1.54193,7,255,0 +773,7.91876,-1.84832,3.53712,-1.54193,7,255,0 +774,7.85918,-1.84832,3.53712,-1.54193,7,255,0 +775,7.79959,-1.84832,3.53712,-1.54193,7,255,0 +776,7.74001,-1.84832,3.53712,-1.54193,7,255,0 +777,7.68043,-1.84832,3.53712,-1.54193,7,255,0 +778,7.62084,-1.84832,3.53712,-1.54193,7,255,0 +779,7.56125,-1.84832,3.53712,-1.54193,7,255,0 +780,7.50167,-1.84832,3.53712,-1.54193,7,255,0 +781,7.44208,-1.84832,3.53712,-1.54193,7,255,0 +782,7.3825,-1.84832,3.53712,-1.54193,7,255,0 +783,7.32291,-1.84832,3.53712,-1.54193,7,255,0 +784,7.26333,-1.84832,3.53712,-1.54193,7,255,0 +785,7.20374,-1.84832,3.53712,-1.54193,7,255,0 +786,7.14416,-1.84832,3.53712,-1.54193,7,255,0 +787,7.08457,-1.84832,3.53712,-1.54193,7,255,0 +788,7.02499,-1.84832,3.53712,-1.54193,7,255,0 +789,6.96541,-1.84832,3.53712,-1.54193,7,255,0 +790,6.90582,-1.84832,3.53712,-1.54193,7,255,0 +791,6.84623,-1.84832,3.53712,-1.54193,7,255,0 +792,6.78665,-1.84833,3.53712,-1.54193,7,255,0 +793,6.72706,-1.84833,3.53712,-1.54193,7,255,0 +794,6.66748,-1.84833,3.53712,-1.54193,7,255,0 +795,6.6079,-1.84833,3.53712,-1.54193,7,255,0 +796,6.54831,-1.84833,3.53712,-1.54193,7,255,0 +797,6.48873,-1.84833,3.53712,-1.54193,7,255,0 +798,6.42914,-1.84833,3.53712,-1.54193,7,255,0 +799,6.36956,-1.84833,3.53712,-1.54193,7,255,0 +800,6.30997,-1.84833,3.53712,-1.54193,7,255,0 +801,6.25038,-1.84833,3.53712,-1.54193,7,255,0 +802,6.1908,-1.84833,3.53712,-1.54193,7,255,0 +803,6.13123,-1.84833,3.53712,-1.54193,7,255,0 +804,6.07158,-1.84833,3.53712,-1.54193,7,255,0 +805,6.01204,-1.84833,3.53712,-1.54193,7,255,0 +806,5.95275,-1.84833,3.53712,-1.54193,7,255,0 +807,5.89215,-1.84833,3.53712,-1.54193,7,255,0 +808,5.83329,-1.84833,3.53712,-1.54193,7,255,0 +809,5.77773,-1.84833,3.53712,-1.54193,7,255,0 +810,5.70396,-1.84833,3.53712,-1.54193,7,255,0 +811,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +812,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +813,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +814,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +815,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +816,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +817,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +818,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +819,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +820,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +821,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +822,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +823,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +824,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +825,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +826,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +827,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +828,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +829,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +830,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +831,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +832,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +833,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +834,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +835,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +836,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +837,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +838,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +839,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +840,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +841,5.65454,-1.84833,3.53712,-1.54193,7,255,0 +842,5.65454,-1.84833,3.53715,-1.54193,7,255,0 +843,5.65454,-1.84833,3.53721,-1.54193,7,255,0 +844,5.65454,-1.84833,3.53726,-1.54193,7,255,0 +845,5.65454,-1.84833,3.5373,-1.54193,7,255,0 +846,5.65454,-1.84833,3.53735,-1.54193,7,255,0 +847,5.65454,-1.84833,3.5374,-1.54193,7,255,0 +848,5.65454,-1.84833,3.53746,-1.54193,7,255,0 +849,5.65454,-1.84833,3.53749,-1.54193,7,255,0 +850,5.65454,-1.84833,3.53751,-1.54193,7,255,0 +851,5.65454,-1.84833,3.53752,-1.54193,7,255,0 +852,5.65454,-1.84833,3.53753,-3.11273,7,255,0 +853,5.65454,-1.84833,3.53754,-3.11273,7,255,0 +854,5.65454,-1.84833,3.53755,-3.11273,7,255,0 +855,5.65454,-1.84833,3.53756,-3.11273,7,255,0 +856,5.65454,-1.84833,3.53756,-3.11273,7,255,0 +857,5.65454,-1.84833,3.53757,-3.11273,7,255,0 +858,5.65454,-1.84833,3.53757,-3.11273,7,255,0 +859,5.65454,-1.84833,3.53758,-3.11273,7,255,0 +860,5.65454,-1.84833,3.53758,-3.11273,7,255,0 +861,5.65454,-1.84833,3.53758,-3.11273,7,255,0 +862,5.65454,-1.84833,3.53758,-3.11273,7,255,0 +863,5.65454,-1.84833,3.53758,-3.11273,7,255,0 +864,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +865,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +866,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +867,5.65454,-1.84833,3.53759,-3.11273,7,255,0 +868,5.65049,-1.82643,3.51012,-3.10193,7,255,0 +869,5.6412,-1.78182,3.454,-3.0815,7,255,0 +870,5.63027,-1.74017,3.4012,-3.06561,7,255,0 +871,5.61879,-1.70323,3.35405,-3.05319,7,255,0 +872,5.60705,-1.66743,3.30825,-3.0404,7,255,0 +873,5.59457,-1.63197,3.26272,-3.02754,7,255,0 +874,5.58083,-1.59674,3.21726,-3.01564,7,255,0 +875,5.56591,-1.56175,3.17188,-3.00459,7,255,0 +876,5.55,-1.52703,3.12665,-2.99406,7,255,0 +877,5.53318,-1.49257,3.08158,-2.984,7,255,0 +878,5.51549,-1.45835,3.03665,-2.97444,7,255,0 +879,5.49698,-1.42437,2.99186,-2.96534,7,255,0 +880,5.47771,-1.39062,2.94722,-2.95664,7,255,0 +881,5.45769,-1.35711,2.90273,-2.94832,7,255,0 +882,5.43697,-1.32382,2.85839,-2.94036,7,255,0 +883,5.41557,-1.29075,2.81421,-2.93275,7,255,0 +884,5.39352,-1.25791,2.77018,-2.92546,7,255,0 +885,5.37084,-1.22528,2.7263,-2.91847,7,255,0 +886,5.34756,-1.19287,2.68258,-2.91176,7,255,0 +887,5.32371,-1.16067,2.63901,-2.90533,7,255,0 +888,5.29931,-1.12867,2.5956,-2.89914,7,255,0 +889,5.27437,-1.09687,2.55233,-2.8932,7,255,0 +890,5.24894,-1.06527,2.50922,-2.88748,7,255,0 +891,5.22301,-1.03387,2.46625,-2.88196,7,255,0 +892,5.19661,-1.00265,2.42343,-2.87665,7,255,0 +893,5.16977,-0.97162,2.38076,-2.87153,7,255,0 +894,5.1425,-0.94076,2.33823,-2.86659,7,255,0 +895,5.11481,-0.91009,2.29584,-2.86182,7,255,0 +896,5.08672,-0.87958,2.25359,-2.85721,7,255,0 +897,5.05825,-0.84924,2.21147,-2.85275,7,255,0 +898,5.02941,-0.81907,2.16949,-2.84843,7,255,0 +899,5.00021,-0.78905,2.12764,-2.84425,7,255,0 +900,4.97068,-0.75919,2.08592,-2.84019,7,255,0 +901,4.94082,-0.72948,2.04432,-2.83626,7,255,0 +902,4.91064,-0.69991,2.00285,-2.83245,7,255,0 +903,4.88015,-0.67049,1.9615,-2.82875,7,255,0 +904,4.84938,-0.64121,1.92026,-2.82515,7,255,0 +905,4.81832,-0.61207,1.87915,-2.82165,7,255,0 +906,4.78698,-0.58306,1.83814,-2.81825,7,255,0 +907,4.75539,-0.55417,1.79725,-2.81494,7,255,0 +908,4.72354,-0.52542,1.75647,-2.81171,7,255,0 +909,4.69144,-0.49678,1.71579,-2.80857,7,255,0 +910,4.65911,-0.46827,1.67521,-2.80551,7,255,0 +911,4.62655,-0.43987,1.63474,-2.80252,7,255,0 +912,4.59377,-0.41158,1.59436,-2.79961,7,255,0 +913,4.56077,-0.38341,1.55408,-2.79676,7,255,0 +914,4.52756,-0.35535,1.5139,-2.79398,7,255,0 +915,4.49413,-0.3274,1.47382,-2.79127,7,255,0 +916,4.46048,-0.29957,1.43385,-2.78864,7,255,0 +917,4.4266,-0.27186,1.39398,-2.78607,7,255,0 +918,4.39247,-0.24428,1.35423,-2.78355,7,255,0 +919,4.3582,-0.21679,1.31456,-2.78112,7,255,0 +920,4.32383,-0.18938,1.27495,-2.77877,7,255,0 +921,4.28909,-0.16211,1.23548,-2.77639,7,255,0 +922,4.2539,-0.13496,1.19615,-2.77395,7,255,0 +923,4.21934,-0.10777,1.15672,-2.77183,7,255,0 +924,4.18494,-0.07986,1.11621,-2.77023,7,255,0 +925,4.14273,-0.04879,1.07106,-2.76811,7,255,0 +926,4.08671,-0.016,1.02331,-2.76493,7,255,0 +927,4.02606,0.0,1.0,-2.76315,7,255,0 +928,3.97066,0.0,1.0,-2.76315,7,255,0 +929,3.91803,0.0,1.0,-2.76315,7,255,0 +930,3.86381,0.0,1.0,-2.76315,7,255,0 +931,3.80863,0.0,1.0,-2.76315,7,255,0 +932,3.75394,0.0,1.0,-2.76315,7,255,0 +933,3.69956,0.0,1.0,-2.76315,7,255,0 +934,3.64504,0.0,1.0,-2.76315,7,255,0 +935,3.59042,0.0,1.0,-2.76315,7,255,0 +936,3.53584,0.0,1.0,-2.76315,7,255,0 +937,3.48129,0.0,1.0,-2.76315,7,255,0 +938,3.42673,0.0,1.0,-2.76315,7,255,0 +939,3.37216,0.0,1.0,-2.76315,7,255,0 +940,3.31759,0.0,1.0,-2.76315,7,255,0 +941,3.26303,0.0,1.0,-2.76315,7,255,0 +942,3.20846,0.0,1.0,-2.76315,7,255,0 +943,3.15389,0.0,1.0,-2.76315,7,255,0 +944,3.09933,0.0,1.0,-2.76315,7,255,0 +945,3.04476,0.0,1.0,-2.76315,7,255,0 +946,2.9902,0.0,1.0,-2.76315,7,255,0 +947,2.93563,0.0,1.0,-2.76315,7,255,0 +948,2.88107,0.0,1.0,-2.76315,7,255,0 +949,2.8265,0.0,1.0,-2.76315,7,255,0 +950,2.77194,0.0,1.0,-2.76315,7,255,0 +951,2.71737,0.0,1.0,-2.76315,7,255,0 +952,2.6628,0.0,1.0,-2.76315,7,255,0 +953,2.60824,0.0,1.0,-2.76315,7,255,0 +954,2.55367,0.0,1.0,-2.76315,7,255,0 +955,2.49911,0.0,1.0,-2.76315,7,255,0 +956,2.44454,0.0,1.0,-2.76315,7,255,0 +957,2.38998,0.0,1.0,-2.76315,7,255,0 +958,2.33541,0.0,1.0,-2.76315,7,255,0 +959,2.28084,0.0,1.0,-2.76315,7,255,0 +960,2.22628,0.0,1.0,-2.76315,7,255,0 +961,2.17171,0.0,1.0,-2.76315,7,255,0 +962,2.11715,0.0,1.0,-2.76315,7,255,0 +963,2.06258,0.0,1.0,-2.76315,7,255,0 +964,2.00802,0.0,1.0,-2.76315,7,255,0 +965,1.95345,0.0,1.0,-2.76315,7,255,0 +966,1.89888,0.0,1.0,-2.76315,7,255,0 +967,1.84432,0.0,1.0,-2.76315,7,255,0 +968,1.78975,0.0,1.0,-2.76315,7,255,0 +969,1.73519,0.0,1.0,-2.76315,7,255,0 +970,1.68063,0.0,1.0,-2.76315,7,255,0 +971,1.62606,0.0,1.0,-2.76315,7,255,0 +972,1.57149,0.0,1.0,-2.76315,7,255,0 +973,1.51693,0.0,1.0,-2.76315,7,255,0 +974,1.46236,0.0,1.0,-2.76315,7,255,0 +975,1.4078,0.0,1.0,-2.76315,7,255,0 +976,1.35323,0.0,1.0,-2.76315,7,255,0 +977,1.29867,0.0,1.0,-2.76315,7,255,0 +978,1.2441,0.0,1.0,-2.76315,7,255,0 +979,1.18953,0.0,1.0,-2.76315,7,255,0 +980,1.13497,0.0,1.0,-2.76315,7,255,0 +981,1.0804,0.0,1.0,-2.76315,7,255,0 +982,1.02584,0.0,1.0,-2.76315,7,255,0 +983,0.97127,0.0,1.0,-2.76315,7,255,0 +984,0.9167,0.0,1.0,-2.76315,7,255,0 +985,0.86214,0.0,1.0,-2.76315,7,255,0 +986,0.80757,0.0,1.0,-2.76315,7,255,0 +987,0.75301,0.0,1.0,-2.76315,7,255,0 +988,0.69844,0.0,1.0,-2.76315,7,255,0 +989,0.64388,0.0,1.0,-2.76315,7,255,0 +990,0.58931,0.0,1.0,-2.76315,7,255,0 +991,0.53475,0.0,1.0,-2.76315,7,255,0 +992,0.48018,0.0,1.0,-2.76315,7,255,0 +993,0.42561,0.0,1.0,-2.76315,7,255,0 +994,0.37105,0.0,1.0,-2.76315,7,255,0 +995,0.31648,0.0,1.0,-2.76315,7,255,0 +996,0.26192,0.0,1.0,-2.76315,7,255,0 +997,0.20735,0.0,1.0,-2.76315,7,255,0 +998,0.15279,0.0,1.0,-2.76315,7,255,0 +999,0.09822,0.0,1.0,-2.76315,7,255,0 +1000,0.04365,0.0,1.0,-2.76315,7,255,0 +1001,-0.01091,0.0,1.0,-2.76315,7,255,0 +1002,-0.06547,0.0,1.0,-2.76315,7,255,0 +1003,-0.12004,0.0,1.0,-2.76315,7,255,0 +1004,-0.17463,0.0,1.0,-2.76315,7,255,0 +1005,-0.22918,0.0,1.0,-2.76315,7,255,0 +1006,-0.28366,0.0,1.0,-2.76315,7,255,0 +1007,-0.33828,0.0,1.0,-2.76315,7,255,0 +1008,-0.39314,0.0,1.0,-2.76315,7,255,0 +1009,-0.44752,0.0,1.0,-2.76315,7,255,0 +1010,-0.50114,0.0,1.0,-2.76315,7,255,0 +1011,-0.55624,0.0,1.0,-2.76315,7,255,0 +1012,-0.61376,0.0,1.0,-2.76315,7,255,0 +1013,-0.66693,0.0,1.0,-2.76315,7,255,0 +1014,-0.7127,0.0,1.0,-2.76315,7,255,0 +1015,-0.77046,0.0,1.0,-2.76315,7,255,0 +1016,-0.85023,0.00069,1.0,-2.76315,7,255,0 +1017,-0.90212,0.0055,1.0,-2.76315,7,255,0 +1018,-0.9114,0.03656,1.0,-2.76315,7,255,0 +1019,-0.91299,0.09409,1.0,-2.76315,7,255,0 +1020,-0.91465,0.15423,1.0,-2.76315,7,255,0 +1021,-0.917,0.21249,1.0,-2.76315,7,255,0 +1022,-0.91909,0.26934,1.0,-2.76315,7,255,0 +1023,-0.92101,0.32571,1.0,-2.76315,7,255,0 +1024,-0.92299,0.3824,1.0,-2.76315,7,255,0 +1025,-0.92502,0.43932,1.0,-2.76315,7,255,0 +1026,-0.92703,0.4962,1.0,-2.76315,7,255,0 +1027,-0.92902,0.55304,1.0,-2.76315,7,255,0 +1028,-0.93103,0.60986,1.0,-2.76315,7,255,0 +1029,-0.93303,0.66669,1.0,-2.76315,7,255,0 +1030,-0.93504,0.72353,1.0,-2.76315,7,255,0 +1031,-0.93704,0.78037,1.0,-2.76315,7,255,0 +1032,-0.93905,0.8372,1.0,-2.76315,7,255,0 +1033,-0.94105,0.89403,1.0,-2.76315,7,255,0 +1034,-0.94306,0.95087,1.0,-2.76315,7,255,0 +1035,-0.94506,1.00771,1.0,-2.76315,7,255,0 +1036,-0.94706,1.06454,1.0,-2.76315,7,255,0 +1037,-0.94907,1.12137,1.0,-2.76315,7,255,0 +1038,-0.95107,1.17821,1.0,-2.76315,7,255,0 +1039,-0.95308,1.23504,1.0,-2.76315,7,255,0 +1040,-0.95508,1.29188,1.0,-2.76315,7,255,0 +1041,-0.95709,1.34871,1.0,-2.76315,7,255,0 +1042,-0.95909,1.40555,1.0,-2.76315,7,255,0 +1043,-0.96109,1.46238,1.0,-2.76315,7,255,0 +1044,-0.9631,1.51922,1.0,-2.76315,7,255,0 +1045,-0.9651,1.57605,1.0,-2.76315,7,255,0 +1046,-0.96711,1.63289,1.0,-2.76315,7,255,0 +1047,-0.96911,1.68972,1.0,-2.76315,7,255,0 +1048,-0.97112,1.74656,1.0,-2.76315,7,255,0 +1049,-0.97312,1.80339,1.0,-2.76315,7,255,0 +1050,-0.97512,1.86024,1.0,-2.76315,7,255,0 +1051,-0.97713,1.91706,1.0,-2.76315,7,255,0 +1052,-0.97913,1.97386,1.0,-2.76315,7,255,0 +1053,-0.98114,2.03073,1.0,-2.76315,7,255,0 +1054,-0.98314,2.08772,1.0,-2.76315,7,255,0 +1055,-0.98515,2.14444,1.0,-2.76315,7,255,0 +1056,-0.98715,2.20073,1.0,-2.76315,7,255,0 +1057,-0.98915,2.2579,1.0,-2.76315,7,255,0 +1058,-0.99115,2.3165,1.0,-2.76315,7,255,0 +1059,-0.99316,2.37257,1.0,-2.76315,7,255,0 +1060,-0.99521,2.42414,1.0,-2.76315,7,255,0 +1061,-0.99521000000000004,2.42414,1.0,0,0,0,0 +1062,-1.0301400000000001,2.4494266666666666,1.0,0,0,0,0 +1063,-1.06507,2.4747133333333333,1.0,0,0,0,0 +1064,-1.1000000000000001,2.5,1.0,0,0,0,0 diff --git a/tests/animation_3.csv b/tests/animation_3.csv new file mode 100644 index 0000000..1f725db --- /dev/null +++ b/tests/animation_3.csv @@ -0,0 +1,12 @@ +route +20,0.0,0.0,1.0,0.0,0,204,2 +21,0.02217,0.0,1.0,0.0,0,204,2 +22,0.08889,0.0,1.0,0.0,0,204,2 +23,0.19737,0.0,1.0,0.0,0,204,2 +24,0.33926,0.0,1.0,0.0,0,204,2 +25,0.5,0.0,1.0,0.0,0,204,2 +26,0.66074,0.0,1.0,0.0,0,204,2 +27,0.80263,0.0,1.0,0.0,0,204,2 +28,0.91111,0.0,1.0,0.0,0,204,2 +29,0.97783,0.0,1.0,0.0,0,204,2 +30,1.0,0.0,1.0,0.0,0,204,2 diff --git a/tests/animation_4.csv b/tests/animation_4.csv new file mode 100644 index 0000000..530caf5 --- /dev/null +++ b/tests/animation_4.csv @@ -0,0 +1,161 @@ +two_drones_test +1,0.2,1.4,1.0,0.15708,0,0,0 +2,0.2,1.4,1.0,0.15708,0,0,0 +3,0.2,1.4,1.0,0.15708,0,0,0 +4,0.2,1.4,1.0,0.15708,0,0,0 +5,0.2,1.4,1.0,0.15708,0,0,0 +6,0.2,1.4,1.0,0.15708,0,0,0 +7,0.2,1.4,1.0,0.15708,0,0,0 +8,0.2,1.4,1.0,0.15708,0,0,0 +9,0.2,1.4,1.0,0.15708,0,0,0 +10,0.2,1.4,1.0,0.15708,0,0,0 +11,0.20441,1.4,1.0,0.15708,0,0,0 +12,0.21774,1.4,1.0,0.15708,0,0,0 +13,0.24002,1.4,1.0,0.15708,0,0,0 +14,0.27111,1.4,1.0,0.15708,0,0,0 +15,0.31063,1.4,1.0,0.15708,0,0,0 +16,0.3579,1.4,1.0,0.15708,0,0,0 +17,0.41193,1.4,1.0,0.15708,0,0,0 +18,0.47141,1.4,1.0,0.15708,0,0,0 +19,0.53472,1.4,1.0,0.15708,0,0,0 +20,0.6,1.4,1.0,0.15708,0,0,0 +21,0.66528,1.4,1.0,0.15708,0,0,0 +22,0.72859,1.4,1.0,0.15708,0,0,0 +23,0.78807,1.4,1.0,0.15708,0,0,0 +24,0.8421,1.4,1.0,0.15708,0,0,0 +25,0.88937,1.4,1.0,0.15708,0,0,0 +26,0.92889,1.4,1.0,0.15708,0,0,0 +27,0.95998,1.4,1.0,0.15708,0,0,0 +28,0.98226,1.4,1.0,0.15708,0,0,0 +29,0.99559,1.4,1.0,0.15708,0,0,0 +30,1.0,1.4,1.0,0.15708,0,0,0 +31,1.0,1.40441,1.0,0.15708,0,0,0 +32,1.0,1.41774,1.0,0.15708,0,0,0 +33,1.0,1.44002,1.0,0.15708,0,0,0 +34,1.0,1.47111,1.0,0.15708,0,0,0 +35,1.0,1.51063,1.0,0.15708,0,0,0 +36,1.0,1.5579,1.0,0.15708,0,0,0 +37,1.0,1.61193,1.0,0.15708,0,0,0 +38,1.0,1.67141,1.0,0.15708,0,0,0 +39,1.0,1.73472,1.0,0.15708,0,0,0 +40,1.0,1.8,1.0,0.15708,0,0,0 +41,1.0,1.86528,1.0,0.15708,0,0,0 +42,1.0,1.92859,1.0,0.15708,0,0,0 +43,1.0,1.98807,1.0,0.15708,0,0,0 +44,1.0,2.0421,1.0,0.15708,0,0,0 +45,1.0,2.08937,1.0,0.15708,0,0,0 +46,1.0,2.12889,1.0,0.15708,0,0,0 +47,1.0,2.15998,1.0,0.15708,0,0,0 +48,1.0,2.18226,1.0,0.15708,0,0,0 +49,1.0,2.19559,1.0,0.15708,0,0,0 +50,1.0,2.2,1.0,0.15708,0,0,0 +51,0.99559,2.2,1.0,0.15708,0,0,0 +52,0.98226,2.2,1.0,0.15708,0,0,0 +53,0.95998,2.2,1.0,0.15708,0,0,0 +54,0.92889,2.2,1.0,0.15708,0,0,0 +55,0.88937,2.2,1.0,0.15708,0,0,0 +56,0.8421,2.2,1.0,0.15708,0,0,0 +57,0.78807,2.2,1.0,0.15708,0,0,0 +58,0.72859,2.2,1.0,0.15708,0,0,0 +59,0.66528,2.2,1.0,0.15708,0,0,0 +60,0.6,2.2,1.0,0.15708,0,0,0 +61,0.53472,2.2,1.0,0.15708,0,0,0 +62,0.47141,2.2,1.0,0.15708,0,0,0 +63,0.41193,2.2,1.0,0.15708,0,0,0 +64,0.3579,2.2,1.0,0.15708,0,0,0 +65,0.31063,2.2,1.0,0.15708,0,0,0 +66,0.27111,2.2,1.0,0.15708,0,0,0 +67,0.24002,2.2,1.0,0.15708,0,0,0 +68,0.21774,2.2,1.0,0.15708,0,0,0 +69,0.20441,2.2,1.0,0.15708,0,0,0 +70,0.2,2.2,1.0,0.15708,0,0,0 +71,0.2,2.19559,1.0,0.15708,0,0,0 +72,0.2,2.18226,1.0,0.15708,0,0,0 +73,0.2,2.15998,1.0,0.15708,0,0,0 +74,0.2,2.12889,1.0,0.15708,0,0,0 +75,0.2,2.08937,1.0,0.15708,0,0,0 +76,0.2,2.0421,1.0,0.15708,0,0,0 +77,0.2,1.98807,1.0,0.15708,0,0,0 +78,0.2,1.92859,1.0,0.15708,0,0,0 +79,0.2,1.86528,1.0,0.15708,0,0,0 +80,0.2,1.8,1.0,0.15708,0,0,0 +81,0.2,1.73472,1.0,0.15708,0,0,0 +82,0.2,1.67141,1.0,0.15708,0,0,0 +83,0.2,1.61193,1.0,0.15708,0,0,0 +84,0.2,1.5579,1.0,0.15708,0,0,0 +85,0.2,1.51063,1.0,0.15708,0,0,0 +86,0.2,1.47111,1.0,0.15708,0,0,0 +87,0.2,1.44002,1.0,0.15708,0,0,0 +88,0.2,1.41774,1.0,0.15708,0,0,0 +89,0.2,1.40441,1.0,0.15708,0,0,0 +90,0.2,1.4,1.0,0.15708,0,0,0 +91,0.2062,1.4,1.0,0.15708,0,0,0 +92,0.22355,1.4,1.0,0.15708,0,0,0 +93,0.25043,1.4,1.0,0.15708,0,0,0 +94,0.28553,1.4,1.0,0.15708,0,0,0 +95,0.32771,1.4,1.0,0.15708,0,0,0 +96,0.3759,1.4,1.0,0.15708,0,0,0 +97,0.42903,1.4,1.0,0.15708,0,0,0 +98,0.48579,1.4,1.0,0.15708,0,0,0 +99,0.54421,1.4,1.0,0.15708,0,0,0 +100,0.6,1.4,1.0,0.15708,0,0,0 +101,0.66257,1.40492,1.025,0.15708,0,0,0 +102,0.72361,1.41958,1.05,0.31416,0,0,0 +103,0.7816,1.4436,1.075,0.47124,0,0,0 +104,0.83511,1.47639,1.1,0.62832,0,0,0 +105,0.88284,1.51716,1.125,0.7854,0,0,0 +106,0.92361,1.56489,1.15,0.94248,0,0,0 +107,0.9564,1.6184,1.175,1.09956,0,0,0 +108,0.98042,1.67639,1.2,1.25664,0,0,0 +109,0.99508,1.73743,1.225,1.41372,0,0,0 +110,1.0,1.8,1.25,1.5708,0,0,0 +111,0.99508,1.86257,1.275,1.72788,0,0,0 +112,0.98042,1.92361,1.3,1.88496,0,0,0 +113,0.9564,1.9816,1.325,2.04204,0,0,0 +114,0.92361,2.03511,1.35,2.19912,0,0,0 +115,0.88284,2.08284,1.375,2.35619,0,0,0 +116,0.83511,2.12361,1.4,2.51327,0,0,0 +117,0.7816,2.1564,1.425,2.67035,0,0,0 +118,0.72361,2.18042,1.45,2.82743,0,0,0 +119,0.66257,2.19508,1.475,2.98451,0,0,0 +120,0.6,2.2,1.5,-3.14159,0,0,0 +121,0.53743,2.19508,1.525,-2.98451,0,0,0 +122,0.47639,2.18042,1.55,-2.82743,0,0,0 +123,0.4184,2.1564,1.575,-2.67035,0,0,0 +124,0.36489,2.12361,1.6,-2.51327,0,0,0 +125,0.31716,2.08284,1.625,-2.35619,0,0,0 +126,0.27639,2.03511,1.65,-2.19911,0,0,0 +127,0.2436,1.9816,1.675,-2.04203,0,0,0 +128,0.21958,1.92361,1.7,-1.88495,0,0,0 +129,0.20492,1.86257,1.725,-1.72788,0,0,0 +130,0.2,1.8,1.75,-1.5708,0,0,0 +131,0.20492,1.73743,1.775,-1.41372,0,0,0 +132,0.21958,1.67639,1.8,-1.25664,0,0,0 +133,0.2436,1.6184,1.825,-1.09956,0,0,0 +134,0.27639,1.56489,1.85,-0.94248,0,0,0 +135,0.31716,1.51716,1.875,-0.7854,0,0,0 +136,0.36489,1.47639,1.9,-0.62832,0,0,0 +137,0.4184,1.4436,1.925,-0.47124,0,0,0 +138,0.47639,1.41958,1.95,-0.31416,0,0,0 +139,0.53743,1.40492,1.975,-0.15708,0,0,0 +140,0.6,1.4,2.0,0.0,0,0,0 +141,0.6,1.4,2.0,0.0,0,0,0 +142,0.6,1.4,2.0,0.0,0,0,0 +143,0.6,1.4,2.0,0.0,0,0,0 +144,0.6,1.4,2.0,0.0,0,0,0 +145,0.6,1.4,2.0,0.0,0,0,0 +146,0.6,1.4,2.0,0.0,0,0,0 +147,0.6,1.4,2.0,0.0,0,0,0 +148,0.6,1.4,2.0,0.0,0,0,0 +149,0.6,1.4,2.0,0.0,0,0,0 +150,0.6,1.4,2.0,0.0,0,0,0 +151,0.6,1.4,2.0,0.0,0,0,0 +152,0.6,1.4,2.0,0.0,0,0,0 +153,0.6,1.4,2.0,0.0,0,0,0 +154,0.6,1.4,2.0,0.0,0,0,0 +155,0.6,1.4,2.0,0.0,0,0,0 +156,0.6,1.4,2.0,0.0,0,0,0 +157,0.6,1.4,2.0,0.0,0,0,0 +158,0.6,1.4,2.0,0.0,0,0,0 +159,0.6,1.4,2.0,0.0,0,0,0 +160,0.6,1.4,2.0,0.0,0,0,0 diff --git a/tests/animation_test.py b/tests/animation_test.py new file mode 100644 index 0000000..ce0abd7 --- /dev/null +++ b/tests/animation_test.py @@ -0,0 +1,140 @@ +import os +import sys +import shutil +from pytest import approx +import pytest + +# Add parent dir to PATH to import config +import inspect +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) +sys.path.insert(0, os.path.join(parent_dir,"Drone")) + +from config import ConfigManager + +config_path = 'animation_config/config' +spec_path = os.path.join(config_path,'spec') +if not os.path.exists(spec_path): + try: + os.makedirs(spec_path) + except OSError: + print("Creation of the directory {} failed".format(spec_path)) + else: + print("Successfully created the directory {}".format(spec_path)) + +shutil.copy("../Drone/config/spec/configspec_client.ini", spec_path) + +config = ConfigManager() +config.load_config_and_spec(os.path.join(config_path,'client.ini')) + +assert config.config_name == "client" + +import animation_lib + +a = animation_lib.Animation() + +def test_animation_1(): + a.update_frames(config, "animation_1.csv") + assert a.id == 'basic' + assert approx(a.original_frames[0].get_pos()) == [0,0,0] + assert a.original_frames[0].get_color() == [204,2,0] + assert a.original_frames[0].pose_is_valid() + assert animation_lib.get_numbers(a.static_begin_frames) == range(1,11) + assert animation_lib.get_numbers(a.takeoff_frames) == range(11,21) + assert animation_lib.get_numbers(a.route_frames) == range(21,31) + assert animation_lib.get_numbers(a.land_frames) == range(31, 41) + assert animation_lib.get_numbers(a.static_end_frames) == range(41, 51) + assert animation_lib.get_numbers(a.output_frames) == range(11,31) + assert approx(a.static_begin_time) == 1 + assert approx(a.takeoff_time) == 1 + assert approx(a.output_frames_min_z) == 0.1 + assert approx(a.get_scaled_output(ratio=[1,2,3], offset=[4,5,6])[0].get_pos()) == [4.,5.,6.3] + assert approx(a.get_scaled_output_min_z(ratio=[1,2,3], offset=[4,5,6])) == 6.3 + assert approx(a.get_start_point(ratio=[1,2,3], offset=[4,5,6])) == [4.,5.,6.3] + +def test_animation_2(): + a.update_frames(config, "animation_2.csv") + assert a.id == 'parad' + assert approx(a.original_frames[271].get_pos()) == [-1.00519,2.65699,0.24386] + assert a.original_frames[271].get_color() == [7,255,0] + assert a.original_frames[271].pose_is_valid() + assert animation_lib.get_numbers(a.static_begin_frames) == range(271) + assert animation_lib.get_numbers(a.takeoff_frames) == range(271,285) + assert animation_lib.get_numbers(a.route_frames) == range(285,1065) + assert animation_lib.get_numbers(a.land_frames) == [] + assert animation_lib.get_numbers(a.static_end_frames) == [] + assert animation_lib.get_numbers(a.output_frames) == range(271, 1065) + assert approx(a.static_begin_time) == 27.1 + assert approx(a.takeoff_time) == 1.4 + assert approx(a.output_frames_min_z) == 0.24386 + assert approx(a.get_scaled_output(ratio=[1,2,3], offset=[4,5,6])[0].get_pos()) == [2.99481, 10.31398, 6.73158] + assert approx(a.get_scaled_output_min_z(ratio=[1,2,3], offset=[4,5,6])) == 6.73158 + assert approx(a.get_start_point(ratio=[1,2,3], offset=[4,5,6])) == [2.99481, 10.31398, 6.73158] + +def test_animation_3(): + a.update_frames(config, "animation_3.csv") + assert a.id == 'route' + assert approx(a.original_frames[9].get_pos()) == [0.97783,0.0,1.0] + assert a.original_frames[9].get_color() == [0,204,2] + assert a.original_frames[9].pose_is_valid() + assert animation_lib.get_numbers(a.static_begin_frames) == [] + assert animation_lib.get_numbers(a.takeoff_frames) == [] + assert animation_lib.get_numbers(a.route_frames) == range(20,31) + assert animation_lib.get_numbers(a.land_frames) == [] + assert animation_lib.get_numbers(a.static_end_frames) == [] + assert approx(a.static_begin_time) == 0 + assert approx(a.takeoff_time) == 0 + assert approx(a.output_frames_min_z) == 1 + assert approx(a.get_scaled_output(ratio=[1,2,3], offset=[4,5,6])[0].get_pos()) == [4,5,9] + assert approx(a.get_scaled_output_min_z(ratio=[1,2,3], offset=[4,5,6])) == 9 + assert approx(a.get_start_point(ratio=[1,2,3], offset=[4,5,6])) == [4,5,9] + +def test_animation_4(): + a.update_frames(config, "animation_4.csv") + assert a.id == 'two_drones_test' + assert approx(a.original_frames[11].get_pos()) == [0.21774,1.4,1.0] + assert a.original_frames[11].get_color() == [0,0,0] + assert a.original_frames[11].pose_is_valid() + assert animation_lib.get_numbers(a.static_begin_frames) == range(1,12) + assert animation_lib.get_numbers(a.takeoff_frames) == [] + assert animation_lib.get_numbers(a.route_frames) == range(12,141) + assert animation_lib.get_numbers(a.land_frames) == [] + assert animation_lib.get_numbers(a.static_end_frames) == range(141,161) + assert animation_lib.get_numbers(a.output_frames) == range(12,141) + assert approx(a.static_begin_time) == 1.1 + assert approx(a.takeoff_time) == 0 + assert approx(a.output_frames_min_z) == 1 + assert approx(a.get_scaled_output(ratio=[1,2,3], offset=[4,5,6])[0].get_pos()) == [4.21774,7.8,9] + assert approx(a.get_scaled_output_min_z(ratio=[1,2,3], offset=[4,5,6])) == 9 + assert approx(a.get_start_point(ratio=[1,2,3], offset=[4,5,6])) == [4.21774,7.8,9] + +def test_animation_no_file(): + a.update_frames(config, "zzz.csv") + assert a.id == None + assert a.original_frames == [] + assert a.output_frames == [] + assert animation_lib.get_numbers(a.static_begin_frames) == [] + assert animation_lib.get_numbers(a.takeoff_frames) == [] + assert animation_lib.get_numbers(a.route_frames) == [] + assert animation_lib.get_numbers(a.land_frames) == [] + assert animation_lib.get_numbers(a.static_end_frames) == [] + assert a.static_begin_time == 0 + assert a.takeoff_time == 0 + assert a.output_frames_min_z is None + assert a.get_scaled_output(ratio=[1,2,3], offset=[4,5,6]) == [] + assert a.get_scaled_output_min_z(ratio=[1,2,3], offset=[4,5,6]) is None + assert a.get_start_point(ratio=[1,2,3], offset=[4,5,6]) == [] + + +# print animation_lib.get_numbers(a.static_begin_frames) +# print animation_lib.get_numbers(a.takeoff_frames) +# print animation_lib.get_numbers(a.route_frames) +# print animation_lib.get_numbers(a.land_frames) +# print animation_lib.get_numbers(a.static_end_frames) +# print animation_lib.get_numbers(a.output_frames) +# print a.static_begin_time +# print a.takeoff_time +# print a.output_frames_min_z + +shutil.rmtree('animation_config') diff --git a/update_configspec.py b/update_configspec.py index 913b0f7..db40009 100644 --- a/update_configspec.py +++ b/update_configspec.py @@ -1,14 +1,34 @@ +import os +import shutil import config from server.copter_table_models import CopterDataModel -cfg_server = config.ConfigObj('SERVER/config/spec/configspec_server.ini', list_values=False) -widths = {"copter_id": 150} -default_width = 100 +from config import ConfigManager, ConfigObj -default = {key: f"preset_param(default=list(True, {widths.get(key, default_width)}))" +config_path = 'temp_config/config' +spec_path = os.path.join(config_path,'spec') +if not os.path.exists(spec_path): + try: + os.makedirs(spec_path) + except OSError: + print("Creation of the directory {} failed".format(spec_path)) + else: + print("Successfully created the directory {}".format(spec_path)) + +shutil.copy("Server/config/spec/configspec_server.ini", spec_path) + +config = ConfigManager() +config.load_config_and_spec(os.path.join(config_path,'server.ini')) + +preset_params = config.table_presets_default +default_param = (True, 100) + +default = {key: f"preset_param(default=list{preset_params.get(key, default_param)})" for key in CopterDataModel.columns} +cfg_server = ConfigObj('Server/config/spec/configspec_server.ini', list_values=False) cfg_server['TABLE']['PRESETS']['DEFAULT'] = default - cfg_server.write() -print('Server configspec updated') + +print('Server configspec updated!') +shutil.rmtree('temp_config')