From 372ce279524cc804860ad64313561017e64e12bd Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Tue, 3 Oct 2023 17:38:12 -0500 Subject: [PATCH 01/24] Initial serial tester --- .../API/services/job_constructor.py | 97 +++++++++++ .../API/services/serial_responder.py | 58 +++++++ Central-Server/API/services/serial_tester.py | 155 ++++++++++++++++++ Central-Server/API/util/__init__.py | 0 Central-Server/API/util/client_packets.py | 74 +++++++++ Central-Server/API/util/packets.py | 68 ++++++++ Test-Rack-Software/TARS-Rack/main.py | 4 +- Test-Rack-Software/TARS-Rack/util/packets.py | 7 +- nginx/logs/nginx.pid | 2 +- 9 files changed, 460 insertions(+), 5 deletions(-) create mode 100644 Central-Server/API/services/job_constructor.py create mode 100644 Central-Server/API/services/serial_responder.py create mode 100644 Central-Server/API/services/serial_tester.py create mode 100644 Central-Server/API/util/__init__.py create mode 100644 Central-Server/API/util/client_packets.py create mode 100644 Central-Server/API/util/packets.py diff --git a/Central-Server/API/services/job_constructor.py b/Central-Server/API/services/job_constructor.py new file mode 100644 index 0000000..f79e389 --- /dev/null +++ b/Central-Server/API/services/job_constructor.py @@ -0,0 +1,97 @@ +# HILSIM Job Constructor +# This service will be responsible for taking in job IDs and sending raw job data over Serial to the Data Streamer app +# running on the Raspberry Pi devices that we're running. +# Michael Karpov (2027) + +import serial_tester +import sys +import serial +import os + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import util.packets as pkt + + + + +comport = "COM8" + +if __name__ == "__main__": + print("Detected script running as __main__, beginning test of Data Streamer functionality") + serial_tester.SECTION("Test setup") + port = serial_tester.TRY_OPEN_PORT(comport) + + # Setup + serial_tester.RESET_TEST(port) + + # > PING + serial_tester.SECTION("PING - Signal testing") + serial_tester.TRY_WRITE(port, pkt.construct_ping().encode(), "Writing PING packet") + serial_tester.TEST("Responds to PING packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, "PONG", valid, type, data)) + + # > IDENT? + serial_tester.SECTION("IDENT? - Identity confirmation") + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet") + serial_tester.TEST("Responds to IDENT? packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + + # > ACK 1 + ack_test_boardid = 4 + serial_tester.SECTION("[1/2] ACK - Valid Acknowledge packet") + serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing valid ACK packet") + serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after ACK packet") + serial_tester.TEST("Responds to IDENT? packet after ACK packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("IDENT? packet after ACK packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + + cond = False + res = "ID-CONF does not return correct board ID after ACK (Expected " + str(ack_test_boardid) + ", but got " + str(data['board_id']) + ")" + if(data['board_id'] == ack_test_boardid): + cond = True + res = "ID-CONF correctly returned board ID " + str(ack_test_boardid) + serial_tester.TEST("Connected board returns properly set ID", (cond, res)) + + # > ACK 2 (invalid) + ack_test_boardid = 0 + serial_tester.SECTION("[2/2] ACK - Acknowledge packet after ACK") + serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing invalid ACK packet") + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("INVALID packet after second ACK packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) + + # > REASSIGN 1 + reassign_test_boardid = 2 + serial_tester.SECTION("REASSIGN - [Valid] Assign new board ID to rack") + serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing REASSIGN packet") + serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after REASSIGN packet") + serial_tester.TEST("Responds to IDENT? packet after REASSIGN packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("IDENT? packet after REASSIGN packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + + cond = False + res = "ID-CONF does not return correct board ID after REASSIGN (Expected " + str(reassign_test_boardid) + ", but got " + str(data['board_id']) + ")" + if(data['board_id'] == reassign_test_boardid): + cond = True + res = "ID-CONF correctly returned board ID " + str(reassign_test_boardid) + serial_tester.TEST("Connected board returns properly set ID", (cond, res)) + + # > REASSIGN 2 + reassign_test_boardid = 8 + serial_tester.RESET_TEST(port) + serial_tester.SECTION("REASSIGN - [Invalid] Assign new board ID to fresh rack") + serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing invalid REASSIGN packet") + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("INVALID packet after non-initialized REASSIGN packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) + + # CLEANUP + serial_tester.SECTION("Cleanup") + serial_tester.TEST("Ensure empty serial bus", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + serial_tester.RESET_TEST(port) + serial_tester.DONE() + diff --git a/Central-Server/API/services/serial_responder.py b/Central-Server/API/services/serial_responder.py new file mode 100644 index 0000000..7ce844e --- /dev/null +++ b/Central-Server/API/services/serial_responder.py @@ -0,0 +1,58 @@ +import serial +import time +import json +import sys +import os + +stored_board_id = -1 + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import util.packets as pkt +import util.client_packets as cl_pkt + +ser = serial.Serial("COM9") +print("connected") + +def tobinary(dict): + return (json.dumps(dict) + "\n").encode() + +if ser.in_waiting: + data = ser.read_all() + print("Cleared " + str(len(data)) + " bytes from memory") + +while True: + if ser.in_waiting: + data = ser.read_all() + string = data.decode("utf8") + print("Got " + string) + + if string: + if string == "!test_reset": + stored_board_id = -1 + print("RESET TEST ENV \n\n") + continue + obj = json.loads(string) + valid, type, data = cl_pkt.decode_packet(string) + + match type: + case "PING": + print("Sending: ", cl_pkt.construct_pong()) + ser.write(tobinary(cl_pkt.construct_pong())) + case "IDENT?": + print("Sending: ", cl_pkt.construct_id_confirm("TARS", stored_board_id)) + ser.write(tobinary(cl_pkt.construct_id_confirm("TARS", stored_board_id))) + case "ACK": + print(stored_board_id) + if(stored_board_id == -1): + stored_board_id = int(data['board_id']) + else: + print("Sending: ", cl_pkt.construct_invalid(string)) + ser.write(tobinary(cl_pkt.construct_invalid(string))) + case "REASSIGN": + if(stored_board_id != -1): + stored_board_id = int(data['board_id']) + else: + print("Sending: ", cl_pkt.construct_invalid(string)) + ser.write(tobinary(cl_pkt.construct_invalid(string))) \ No newline at end of file diff --git a/Central-Server/API/services/serial_tester.py b/Central-Server/API/services/serial_tester.py new file mode 100644 index 0000000..af8aed8 --- /dev/null +++ b/Central-Server/API/services/serial_tester.py @@ -0,0 +1,155 @@ +# A small library to test for responses from serial when sent specific data. +import serial +import time +import traceback +import json +import sys +import os + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import util.packets as pkt +import util.client_packets as cl_pkt + +success = 0 +fails = 0 + +cur_wait_text = "" + +test_results = [] +def add_test_result(text): + test_results.append(text) + +def display(result): + global success, fails + print("\n") + print("Job Constructor Serial Test Results\n") + for test in test_results: + print(test) + print("\n") + if(result == False): + print("SERIAL TEST \033[91mFAIL\033[0m") + print("Test breakdown: \033[92m" + str(success) + " passed\033[0m, \033[91m" + str(fails) + " failed\033[0m\n") + else: + print("ALL TESTS \033[92mSUCCESSFUL\033[0m") + print("Test breakdown: \033[92m" + str(success) + " passed\033[0m\n") + +def TRY_WRITE(port: serial.Serial, data, component): + wait_text(port.name, "Attempting to write " + str(len(data)) + " bytes (" + component + ")") + try: + port.write(data) + PASS("Write:" + port.name, component) + except serial.SerialTimeoutException as exc: + FAIL("Write:" + port.name, "Timeout while writing data (" + component + ")") + +def RESET_TEST(port: serial.Serial): + port.write("!test_reset".encode()) + +def FAIL(component, reason): + global fails, cur_wait_text + print("\033[91mX\033[0m" + cur_wait_text.replace("#", "")) + add_test_result(" \033[91mX FAIL\033[0m (\033[90m" + component + "\033[0m) " + reason) + fails += 1 + display(False) + exit(255) + +def PASS(component, test_text): + global success, cur_wait_text + print("\033[92m✓\033[0m" + cur_wait_text.replace("#", "")) + res = " \033[92m✓ PASS\033[0m (\033[90m" + component + "\033[0m) " + test_text + success += 1 + add_test_result(res) + return res + + +def SECTION(text): + res = "> \033[90m" + text + "\033[0m:" + add_test_result(res) + return res + +def DONE(): + display(True) + exit(0) + +def wait_text(component, text): + global cur_wait_text + cur_wait_text = " # \033[90m(" + component + ")\033[0m " + text + print(cur_wait_text, end="\r") + + +def TEST(component, condition): + if(condition[0]): + PASS(component, condition[1]) + else: + FAIL(component, condition[1]) + + +def AWAIT_ANY_RESPONSE(port: serial.Serial, timeout=3.0): + wait_text("COM:" + port.name, "Awaiting a response from port..") + try: + start_time = time.time() + while(time.time() - start_time < timeout): + if port.in_waiting: + return True, "AWAIT_ANY_RESPONSE recieved a response." + return False, "AWAIT_ANY_RESPONSE timed out while waiting for serial data" + except: + return False, "AWAIT_ANY_RESPONSE ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m" + +def ENSURE_NO_RESPONSE(port: serial.Serial, timeout=3.0): + wait_text("COM:" + port.name, "Checking that the application doesn't respond.") + try: + start_time = time.time() + while(time.time() - start_time < timeout): + if port.in_waiting: + return False, "ENSURE_NO_RESPONSE recieved a response." + return True, "ENSURE_NO_RESPONSE succeeded, no data was transferred." + except: + return False, "ENSURE_NO_RESPONSE ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m" + + +def TRY_OPEN_PORT(port_name): + wait_text(port_name, "Attempting to open port") + try: + port = serial.Serial(port_name, timeout=5, write_timeout=2) + PASS("COMPORT " + port_name, "Successfully connected to COM:" + port_name) + return port + except: + FAIL("COMPORT " + port_name, "Unable to open port:\033[90m\n\n" + traceback.format_exc() + "\033[0m") + +def GET_PACKET(port: serial.Serial, timeout=3.0): + wait_text("Read:" + port.name, "Decoding Serial Packet") + + try: + start_time = time.time() + while(time.time() - start_time < timeout): + if port.in_waiting: + data = port.read_until(b"\n") + string = data.decode("utf8") + + if string: + valid, type, data = pkt.decode_packet(string) + if(valid): + return valid, type, data + else: + FAIL("Read:" + port.name, "Failed to decode packet") + + FAIL("Read:" + port.name, "Read timeout") + except: + FAIL("Read:" + port.name, "VALID_FORMAT ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") + +# Reads last packet and detects if it's of a valid format +def VALID_PACKET(port: serial.Serial, packet_type, valid, type, data, timeout=3.0): + wait_text("Check packet format", "Checking validity of " + packet_type + " packet") + + try: + if(type == packet_type): + if(valid): + return True, "Packet complies to type " + packet_type + else: + return False, "VALID_FORMAT FAILED for " + packet_type + ", got \033[90m\n\n" + type + ": " + data + "\033[0m" + else: + return False, "VALID_FORMAT recieved a different packet type than " + packet_type + except: + return False, "VALID_FORMAT ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m" + \ No newline at end of file diff --git a/Central-Server/API/util/__init__.py b/Central-Server/API/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Central-Server/API/util/client_packets.py b/Central-Server/API/util/client_packets.py new file mode 100644 index 0000000..4261f44 --- /dev/null +++ b/Central-Server/API/util/client_packets.py @@ -0,0 +1,74 @@ +# Contains all communication packets required for server-datastreamer communication. +# Contains constructor functions for each packet and also a decode function for server packets. + +import json + +#### CLIENT PACKETS #### +def construct_ident(board_type: str): + # Constructs IDENT packet + packet_dict = {'type': "IDENT", 'data': {'board_type': board_type}} + return json.dumps(packet_dict) + +def construct_id_confirm(board_type: str, board_id: int): + # Constructs ID-CONF packet + packet_dict = {'type': "ID-CONF", 'data': {'board_type': board_type, 'board_id': board_id}} + return json.dumps(packet_dict) + +def construct_ready(): + # Constructs READY packet + packet_dict = {'type': "READY", 'data': {}} + return json.dumps(packet_dict) + +def construct_done(job_data, hilsim_result: str): + # Constructs DONE packet + packet_dict = {'type': "DONE", 'data': {'job_data': job_data, 'hilsim_result': hilsim_result}} + return json.dumps(packet_dict) + +def construct_invalid(raw_packet): + # Constructs INVALID packet + packet_dict = {'type': "INVALID", 'data': {'raw_packet': raw_packet}} + return json.dumps(packet_dict) + +def construct_busy(job_data): + # Constructs BUSY packet + packet_dict = {'type': "BUSY", 'data': {'job_data': job_data}} + return json.dumps(packet_dict) + +def construct_job_update(job_status, current_log: str): + # Constructs JOB-UPD packet + packet_dict = {'type': "JOB-UPD", 'data': {'job_status': job_status, 'hilsim_result': current_log}} + return json.dumps(packet_dict) + +def construct_pong(): + # Constructs PONG packet + packet_dict = {'type': "PONG", 'data': {}} + return json.dumps(packet_dict) + + +#### Intermediate data #### +def construct_job_status(job_ok, current_action, status_text): + return {"job_ok": job_ok, 'current_action': current_action, "status": status_text} + +#### SERVER PACKETS #### +def decode_packet(packet: str): + packet_dict = json.loads(packet) + return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] + +def validate_server_packet(packet_type: str, packet_data): + match packet_type: + case "IDENT?": + return True + case "ACK": + return "board_id" in packet_data + case "REASSIGN": + return "board_id" in packet_data + case "TERMINATE": + return True + case "CYCLE": + return True + case "JOB": + return "job_data" in packet_data and "csv_data" in packet_data + case "PONG": + return True + + \ No newline at end of file diff --git a/Central-Server/API/util/packets.py b/Central-Server/API/util/packets.py new file mode 100644 index 0000000..4d36134 --- /dev/null +++ b/Central-Server/API/util/packets.py @@ -0,0 +1,68 @@ +# Contains all communication packets required for the SERVER side of the server-datastreamer communication. +# Contains constructor functions for each packet and also a decode function for client packets. +import json + +#### SERVER PACKETS #### +def construct_ident_probe(): + # Constructs IDENT? packet + packet_dict = {'type': "IDENT?", 'data': {}} + return json.dumps(packet_dict) + +def construct_ping(): + # Constructs PING packet + packet_dict = {'type': "PING", 'data': {}} + return json.dumps(packet_dict) + +def construct_acknowledge(board_id: int): + # Constructs ACK packet + packet_dict = {'type': "ACK", 'data': {'board_id': board_id}} + return json.dumps(packet_dict) + +def construct_reassign(board_id: int): + # Constructs ACK packet + packet_dict = {'type': "REASSIGN", 'data': {'board_id': board_id}} + return json.dumps(packet_dict) + +def construct_terminate(): + # Constructs TERMINATE packet + packet_dict = {'type': "TERMINATE", 'data': {}} + return json.dumps(packet_dict) + +def construct_cycle(): + # Constructs TERMINATE packet + packet_dict = {'type': "CYCLE", 'data': {}} + return json.dumps(packet_dict) + +def construct_job(job_data, flight_csv): + # Constructs TERMINATE packet + packet_dict = {'type': "JOB", 'data': {'job_data': job_data, 'sim_data': flight_csv}} + return json.dumps(packet_dict) + +#### CLIENT PACKETS #### +def decode_packet(packet: str): + packet_dict = json.loads(packet) + if(type(packet_dict) == str): + packet_dict = json.loads(packet_dict) + return validate_client_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] + +def validate_client_packet(packet_type: str, packet_data): + match packet_type: + case "IDENT": + return "board_type" in packet_data + case "ID-CONF": + return "board_id" in packet_data and "board_type" in packet_data + case "READY": + return True + case "DONE": + return "hilsim_result" in packet_data and "job_data" in packet_data + case "JOB-UPD": + return "hilsim_result" in packet_data and "job_status" in packet_data + case "INVALID": + return "raw_packet" in packet_data + case "BUSY": + return "job_data" in packet_data + case "PONG": + return True + + + \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index 58b06b5..2bd3d38 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -37,9 +37,7 @@ def main(): pio.pio_clean() git.remote_pull_branch("AV-999/protobuf-integration") - port = getfirstport() - if(not port): - return + try: pio.pio_upload("mcu_hilsim") diff --git a/Test-Rack-Software/TARS-Rack/util/packets.py b/Test-Rack-Software/TARS-Rack/util/packets.py index c410d84..56edaaa 100644 --- a/Test-Rack-Software/TARS-Rack/util/packets.py +++ b/Test-Rack-Software/TARS-Rack/util/packets.py @@ -39,6 +39,11 @@ def construct_job_update(job_status, current_log: str): packet_dict = {'type': "JOB-UPD", 'data': {'job_status': job_status, 'hilsim_result': current_log}} return json.dumps(packet_dict) +def construct_pong(): + # Constructs PONG packet + packet_dict = {'type': "PONG", 'data': {}} + return json.dumps(packet_dict) + #### Intermediate data #### def construct_job_status(job_ok, current_action, status_text): return {"job_ok": job_ok, 'current_action': current_action, "status": status_text} @@ -46,7 +51,7 @@ def construct_job_status(job_ok, current_action, status_text): #### SERVER PACKETS #### def decode_packet(packet: str): packet_dict = json.loads(packet) - return validate_server_packet(packet_dict.type, packet_dict.data), packet_dict.type, packet_dict.data + return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] def validate_server_packet(packet_type: str, packet_data): match packet_type: diff --git a/nginx/logs/nginx.pid b/nginx/logs/nginx.pid index 07bb70d..f19a05e 100644 --- a/nginx/logs/nginx.pid +++ b/nginx/logs/nginx.pid @@ -1 +1 @@ -32784 +13304 From 0baba20a157ed02e5ccbf48de239d7069e29a170 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Tue, 3 Oct 2023 18:35:04 -0500 Subject: [PATCH 02/24] Changed requirements and updated server tester --- Test-Rack-Software/TARS-Rack/README.md | 3 ++- Test-Rack-Software/TARS-Rack/av_platform/stream_data.py | 5 ++++- nginx/logs/access.log | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Test-Rack-Software/TARS-Rack/README.md b/Test-Rack-Software/TARS-Rack/README.md index 13d7957..9093917 100644 --- a/Test-Rack-Software/TARS-Rack/README.md +++ b/Test-Rack-Software/TARS-Rack/README.md @@ -20,12 +20,13 @@ The rest of `av_platform/run_setup.py` -- Implement platform-specific code pushi Here are your technical requirements for server communication: - Whenever an `IDENT?` packet is recieved, IF this board was assigned an id before, send an ID-CONF packet with the board type and board id stored. OTHERWISE, send an IDENT packet. -- Whenever an `ACK` packet is recieved, store the board ID assigned and which port sent it (This will be the server port). +- Whenever an `ACK` packet is recieved, store the board ID assigned and which port sent it (This will be the server port). Then, send a `READY` packet. - Whenever a `REASSIGN` packet is recieved, change to board_id but do not terminate any jobs. - Whenever a `TERMINATE` packet is recieved, immediately terminate all currently running jobs, then, send a `READY` packet. - Whenever a `CYCLE` packet is recieved, terminate all currently running jobs. If the current platform supports power cycling, then power cycle the test stand, then send a `READY` packet. If the current platform cannot power cycle, immediately send a `READY` packet. - `stream_data` runs in a while loop, so to implement the above two bullet points you will need to figure out how to communicate to the server while running a hilsim job. (I don't know exactly how to do that so go ham) - Whenever a `JOB` packet is recieved, IF a job is currently running, send a `BUSY` packet back with the currently running job data. Otherwise, run the job and send `JOB-UPD` packets with the status of the job while it's running (The first status should always be `"Accepted"`) +- Whenever a `PING` packet is recieved, send a `PONG` packet immediately. Most of the places where code needs to be implemented is marked with a `TODO`. Good luck! diff --git a/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py b/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py index 91c32ea..6817d89 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py +++ b/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py @@ -38,7 +38,10 @@ def run_hilsim(raw_csv: str, serial_port: serial.Serial, update_callback): watchdog_start = time.time() cur_line = 0 - while(True): + while(True): + + # listenAndHandleServerPackets() + if(abs(watchdog_start - time.time()) > 3): print("Watchdog timer tripped") return hilsim_return_log diff --git a/nginx/logs/access.log b/nginx/logs/access.log index e6f39c6..a850f38 100644 --- a/nginx/logs/access.log +++ b/nginx/logs/access.log @@ -411,3 +411,12 @@ 127.0.0.1 - - [30/Sep/2023:17:16:08 -0500] "GET /api/ HTTP/1.1" 200 2 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" 127.0.0.1 - - [30/Sep/2023:17:16:11 -0500] "GET /wsstatus/ HTTP/1.1" 200 865 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" 127.0.0.1 - - [30/Sep/2023:17:16:11 -0500] "GET /static/js/bundle.js HTTP/1.1" 304 0 "http://localhost/wsstatus/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:13:51 -0500] "GET / HTTP/1.1" 200 865 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:13:51 -0500] "GET /static/js/bundle.js HTTP/1.1" 200 457660 "http://localhost/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:13:51 -0500] "GET /logo192.png HTTP/1.1" 499 0 "http://localhost/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:13:51 -0500] "GET /favicon.ico HTTP/1.1" 499 0 "http://localhost/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:13:59 -0500] "GET /api HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:13:59 -0500] "GET /api/ HTTP/1.1" 200 2 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:14:06 -0500] "GET /api/wheabwiefhaouwef HTTP/1.1" 404 207 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:14:13 -0500] "GET /wsstatus HTTP/1.1" 200 865 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" +127.0.0.1 - - [03/Oct/2023:17:14:13 -0500] "GET /static/js/bundle.js HTTP/1.1" 304 0 "http://localhost/wsstatus" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0" From d11ef2c90e9b4feb906955c5a83acf1807210501 Mon Sep 17 00:00:00 2001 From: deeya-bodas <90807727+deeya-bodas@users.noreply.github.com> Date: Tue, 3 Oct 2023 18:57:20 -0500 Subject: [PATCH 03/24] test to see if i have push perms --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a8973c..7ec8d6d 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,6 @@ If you have run the server in the `dev` environment, all aspects of the running Sometimes, Nginx will keep running even if the process that started it is killed. Usually, Nginx processes are killed with the `nginx -s stop` command. -To force-kill all nginx process (if `-s stop` stop doesn't work): `taskkill /f /IM nginx.exe` \ No newline at end of file +To force-kill all nginx process (if `-s stop` stop doesn't work): `taskkill /f /IM nginx.exe` + +# test \ No newline at end of file From 23dc02ff44a1d25702e70cc6a0b9be415a184b86 Mon Sep 17 00:00:00 2001 From: deeya-bodas <90807727+deeya-bodas@users.noreply.github.com> Date: Tue, 3 Oct 2023 18:58:32 -0500 Subject: [PATCH 04/24] test to see if i have push perms part reverted --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7ec8d6d..9a8973c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,4 @@ If you have run the server in the `dev` environment, all aspects of the running Sometimes, Nginx will keep running even if the process that started it is killed. Usually, Nginx processes are killed with the `nginx -s stop` command. -To force-kill all nginx process (if `-s stop` stop doesn't work): `taskkill /f /IM nginx.exe` - -# test \ No newline at end of file +To force-kill all nginx process (if `-s stop` stop doesn't work): `taskkill /f /IM nginx.exe` \ No newline at end of file From 19d3b9cf7e155353276fe99824658de30453c685 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 4 Oct 2023 15:13:25 -0500 Subject: [PATCH 05/24] Added initial api files --- .../API/services/datastreamer_test.py | 96 +++++++++++++++++++ .../API/services/hilsim_constructor.py | 19 ++++ Central-Server/API/services/job_creator.py | 0 Central-Server/API/services/job_queue.py | 0 .../API/services/queue_reconstructor.py | 0 Central-Server/API/util/test_config.py | 3 + 6 files changed, 118 insertions(+) create mode 100644 Central-Server/API/services/datastreamer_test.py create mode 100644 Central-Server/API/services/hilsim_constructor.py create mode 100644 Central-Server/API/services/job_creator.py create mode 100644 Central-Server/API/services/job_queue.py create mode 100644 Central-Server/API/services/queue_reconstructor.py create mode 100644 Central-Server/API/util/test_config.py diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py new file mode 100644 index 0000000..a824f42 --- /dev/null +++ b/Central-Server/API/services/datastreamer_test.py @@ -0,0 +1,96 @@ +# HILSIM Datastreamer Test +# This will test all of the datastreamer functionality +# Michael Karpov (2027) + +import serial_tester +import sys +import serial +import os + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import util.packets as pkt + + + + +comport = "COM8" + +if __name__ == "__main__": + print("Detected script running as __main__, beginning test of Data Streamer functionality") + serial_tester.SECTION("Test setup") + port = serial_tester.TRY_OPEN_PORT(comport) + + # Setup + serial_tester.RESET_TEST(port) + + # > PING + serial_tester.SECTION("PING - Signal testing") + serial_tester.TRY_WRITE(port, pkt.construct_ping().encode(), "Writing PING packet") + serial_tester.TEST("Responds to PING packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, "PONG", valid, type, data)) + + # > IDENT? + serial_tester.SECTION("IDENT? - Identity confirmation") + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet") + serial_tester.TEST("Responds to IDENT? packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + + # > ACK 1 + ack_test_boardid = 4 + serial_tester.SECTION("[1/2] ACK - Valid Acknowledge packet") + serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing valid ACK packet") + serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after ACK packet") + serial_tester.TEST("Responds to IDENT? packet after ACK packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("IDENT? packet after ACK packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + + cond = False + res = "ID-CONF does not return correct board ID after ACK (Expected " + str(ack_test_boardid) + ", but got " + str(data['board_id']) + ")" + if(data['board_id'] == ack_test_boardid): + cond = True + res = "ID-CONF correctly returned board ID " + str(ack_test_boardid) + serial_tester.TEST("Connected board returns properly set ID", (cond, res)) + + # > ACK 2 (invalid) + ack_test_boardid = 0 + serial_tester.SECTION("[2/2] ACK - Acknowledge packet after ACK") + serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing invalid ACK packet") + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("INVALID packet after second ACK packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) + + # > REASSIGN 1 + reassign_test_boardid = 2 + serial_tester.SECTION("REASSIGN - [Valid] Assign new board ID to rack") + serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing REASSIGN packet") + serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after REASSIGN packet") + serial_tester.TEST("Responds to IDENT? packet after REASSIGN packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("IDENT? packet after REASSIGN packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + + cond = False + res = "ID-CONF does not return correct board ID after REASSIGN (Expected " + str(reassign_test_boardid) + ", but got " + str(data['board_id']) + ")" + if(data['board_id'] == reassign_test_boardid): + cond = True + res = "ID-CONF correctly returned board ID " + str(reassign_test_boardid) + serial_tester.TEST("Connected board returns properly set ID", (cond, res)) + + # > REASSIGN 2 + reassign_test_boardid = 8 + serial_tester.RESET_TEST(port) + serial_tester.SECTION("REASSIGN - [Invalid] Assign new board ID to fresh rack") + serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing invalid REASSIGN packet") + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("INVALID packet after non-initialized REASSIGN packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) + + # CLEANUP + serial_tester.SECTION("Cleanup") + serial_tester.TEST("Ensure empty serial bus", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + serial_tester.RESET_TEST(port) + serial_tester.DONE() + diff --git a/Central-Server/API/services/hilsim_constructor.py b/Central-Server/API/services/hilsim_constructor.py new file mode 100644 index 0000000..ba1e352 --- /dev/null +++ b/Central-Server/API/services/hilsim_constructor.py @@ -0,0 +1,19 @@ +# HILSIM Job Constructor +# This service will be responsible for taking in job IDs and sending raw job data over Serial to the Data Streamer app +# running on the Raspberry Pi devices that we're running. +# Michael Karpov (2027) + +import serial_tester +import sys +import serial +import os + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import util.packets as pkt + + + +if __name__ == "__main__": + pass \ No newline at end of file diff --git a/Central-Server/API/services/job_creator.py b/Central-Server/API/services/job_creator.py new file mode 100644 index 0000000..e69de29 diff --git a/Central-Server/API/services/job_queue.py b/Central-Server/API/services/job_queue.py new file mode 100644 index 0000000..e69de29 diff --git a/Central-Server/API/services/queue_reconstructor.py b/Central-Server/API/services/queue_reconstructor.py new file mode 100644 index 0000000..e69de29 diff --git a/Central-Server/API/util/test_config.py b/Central-Server/API/util/test_config.py new file mode 100644 index 0000000..2d3e949 --- /dev/null +++ b/Central-Server/API/util/test_config.py @@ -0,0 +1,3 @@ + + +comport = "COM8" \ No newline at end of file From 6e69ea11a75a371b4c4a3139f9411e9228c695ce Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Thu, 5 Oct 2023 12:53:26 -0500 Subject: [PATCH 06/24] Minor restructure --- .../API/services/datastreamer_test.py | 2 +- .../TARS-Rack/av_platform/README.md | 1 + .../TARS-Rack/av_platform/interface.py | 149 ++++++++++++++++++ .../TARS-Rack/av_platform/run_setup.py | 50 ------ .../TARS-Rack/av_platform/stream_data.py | 5 +- Test-Rack-Software/TARS-Rack/main.py | 95 ++++++++--- Test-Rack-Software/TARS-Rack/sv_pkt.py | 68 ++++++++ .../TARS-Rack/test_server_delete.py | 32 ++++ .../TARS-Rack/util/serial_wrapper.py | 36 ++++- 9 files changed, 358 insertions(+), 80 deletions(-) create mode 100644 Test-Rack-Software/TARS-Rack/av_platform/interface.py delete mode 100644 Test-Rack-Software/TARS-Rack/av_platform/run_setup.py create mode 100644 Test-Rack-Software/TARS-Rack/sv_pkt.py create mode 100644 Test-Rack-Software/TARS-Rack/test_server_delete.py diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index a824f42..4436e4d 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -15,7 +15,7 @@ -comport = "COM8" +comport = "COM9" if __name__ == "__main__": print("Detected script running as __main__, beginning test of Data Streamer functionality") diff --git a/Test-Rack-Software/TARS-Rack/av_platform/README.md b/Test-Rack-Software/TARS-Rack/av_platform/README.md index 40d4253..5554b61 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/README.md +++ b/Test-Rack-Software/TARS-Rack/av_platform/README.md @@ -3,5 +3,6 @@ This directory contains all platform-specific tools to do the following two actions: 1) Build and flash the code to the avionics stack 2) Run HILSIM on the stack +3) Provide an interface that will help communication with the main server. Code in this directory **will** be different for each avionics stack. \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/av_platform/interface.py b/Test-Rack-Software/TARS-Rack/av_platform/interface.py new file mode 100644 index 0000000..2240124 --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/av_platform/interface.py @@ -0,0 +1,149 @@ +# AV Interface (TARS) +# This is the specific interface.py file for the TARS avionics stack. +# @implements +# @implements first_setup() -- Performs the first setup (assuming fresh install). +# @implements code_reset() -- Resets the project to a "known" state (Usually master branch). +# @implements code_pull(str git_branch) -- Sets up the project to do a code flash. +# @implements code_flash() -- Flashes the code to the avionics stack +# @implements HilsimRun -- Class that stores the data for a Hilsim run + +import util.git_commands as git +import util.pio_commands as pio +import util.serial_wrapper as server +import av_platform.csv_datastream as csv_datastream +import pandas +import io +import time +import serial +import util.packets as packet + +ready = False # Is the stack ready to recieve data? +TARS_port: serial.Serial = None + +"""This function will check if there's any serial plugged into this board that is NOT the server. +TARS only has one board, so we're good if we see any other port""" +def detect_avionics(ignore_ports: list[serial.Serial]): + for comport in server.connected_comports: + if not (comport in ignore_ports): + if(comport.in_waiting): + TARS_port = comport + ready = True + ready = False + + +""" +This function must be implemented in all run_setup.py functions for each stack +first_setup(): Installs repository and sets up all actions outside of the repository to be ready to accept inputs. +""" +def first_setup(): + git.remote_clone() + git.remote_reset() + +""" +This function must be implemented in all run_setup.py functions for each stack +code_reset(): Resets the repository to a default state +TARS: Resets the TARS-Software repository to master branchd +""" +def code_reset(): + git.remote_reset() + +""" +This function must be implemented in all run_setup.py functions for each stack +code_pull(str git_branch): Stashes changes and pulls a specific branch. +""" +def code_pull(git_branch: str): + git.remote_pull_branch(git_branch) + +""" +This function must be implemented in all run_setup.py functions for each stack +code_flash(): Flashes currently staged code to the avionics stack. +TARS: Uses environment mcu_hilsim +""" +def code_flash(): + # Clean build dir + pio.pio_clean() + # For TARS, we need to attempt the code flash twice, since it always fails the first time. + try: + pio.pio_upload("mcu_hilsim") + except: + pio.pio_upload("mcu_hilsim") + + +""" +This object stores all of the required information and functionality of a HILSIM run. This class +must be implemented in all run_setup.py scripts. +""" +class HilsimRun: + return_log = [] + flight_data_raw = "" + flight_data_dataframe = None + flight_data_rows = None + current_line = 0 + last_packet_time = 0 + start_time = 0 + current_time = 0 + port = None + + # Turns a raw CSV string to a Pandas dataframe + def raw_csv_to_dataframe(raw_csv) -> pandas.DataFrame: + # Get column names + header = raw_csv.split('\n')[0].split(",") + csv = "\n".join(raw_csv.split('\n')[1:]) + csvStringIO = io.StringIO(csv) + return pandas.read_csv(csvStringIO, sep=",", header=None, names=header) + + # Initializes the HILSIM run object + def __init__(self, raw_csv: str, serial_port: serial.Serial) -> None: + self.flight_data_raw = raw_csv + self.flight_data_dataframe = self.raw_csv_to_dataframe(self.flight_data_raw) + self.flight_data_rows = self.flight_data_dataframe.iterrows() + self.port = serial_port + self.start_time = time.time() + self.current_time = self.start_time + self.last_packet_time = self.start_time + + """ + Runs one iteration of the HILSIM loop, with a change in time of dt. + callback_func is a function to communicate back to the main process mid-step. + + @Returns a tuple: (run_finished, run_errored, return_log) + """ + def step(self, dt: float, callback_func): + self.current_time += dt + if self.current_time > self.last_packet_time + 10: + self.last_packet_time += 10 + if self.current_time < self.start_time + 5: + # Wait for 5 seconds to make sure serial is connected + pass + else: + if(self.current_line == 0): + callback_func(packet.construct_job_status(True, "running", f"Running (Data streaming started)")) + self.current_line += 1 + + if(self.current_line % 300 == 0): + # Only send a job update every 3-ish seconds + callback_func(packet.construct_job_status(True, "running", f"Running ({self.current_line/len(self.flight_data_dataframe)*100:.2f}%) [{self.current_line} processed out of {len(self.flight_data_dataframe)} total]")) + + line_num, row = next(self.flight_data_rows, (None, None)) + if line_num == None: + callback_func(packet.construct_job_status(True, "done", f"Finished data streaming")) + return True, False, self.return_log # Finished, No Error, Log + data = csv_datastream.csv_line_to_protobuf(row) + if not data: + callback_func(packet.construct_job_status(True, "error", f"Expected data to insert, but found none.")) + return True, False, self.return_log # Finished, Error, Log + try: + self.port.write(data) + except: + callback_func(packet.construct_job_status(True, "error", f"Exception during serial write: " + traceback.format_exc())) + return True, False, self.return_log # Finished, Error, Log + + if self.port.in_waiting: + data = self.port.read_all() + string = data.decode("utf8") + if string: + string = string[0 : (len(string)-1)] + self.return_log.append(string) + + + diff --git a/Test-Rack-Software/TARS-Rack/av_platform/run_setup.py b/Test-Rack-Software/TARS-Rack/av_platform/run_setup.py deleted file mode 100644 index 8c77c62..0000000 --- a/Test-Rack-Software/TARS-Rack/av_platform/run_setup.py +++ /dev/null @@ -1,50 +0,0 @@ -# RUN SETUP (TARS) -# This is the specific run_setup.py file for the TARS avionics stack. -# @implements first_setup() -- Performs the first setup (assuming fresh install). -# @implements code_reset() -- Resets the project to a "known" state (Usually master branch). -# @implements code_pull(str git_branch) -- Sets up the project to do a code flash. -# @implements code_flash() -- Flashes the code to the avionics stack -# @implements run_hilsim(str raw_csv) -- Runs HILSIM with flashed code and returns a string result - -import util.git_commands as git - -""" -This function must be implemented in all run_setup.py functions for each stack -first_setup(): Installs repository and sets up all actions outside of the repository to be ready to accept inputs. -""" -def first_setup(): - git.remote_clone() - -""" -This function must be implemented in all run_setup.py functions for each stack -code_reset(): Resets the repository to a default state -TARS: Resets the TARS-Software repository to master branchd -""" -def code_reset(): - git.remote_reset() - -""" -This function must be implemented in all run_setup.py functions for each stack -code_pull(str git_branch): Stashes changes and pulls a specific branch. -""" -def code_pull(git_branch: str): - git.remote_pull_branch(git_branch) - -""" -This function must be implemented in all run_setup.py functions for each stack -code_flash(): Flashes currently staged code to the avionics stack. -TARS: Uses environment mcu_hilsim -""" -def code_flash(): - # TODO: Implement - pass - -""" -This function must be implemented in all run_setup.py functions for each stack -run_hilsim(str raw_csv): Runs the HILSIM program on the current avionics stack with the given flight data -""" -def run_hilsim(): - # TODO: Implement - pass - - diff --git a/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py b/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py index 6817d89..91e9f2a 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py +++ b/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py @@ -38,10 +38,7 @@ def run_hilsim(raw_csv: str, serial_port: serial.Serial, update_callback): watchdog_start = time.time() cur_line = 0 - while(True): - - # listenAndHandleServerPackets() - + while(True): if(abs(watchdog_start - time.time()) > 3): print("Watchdog timer tripped") return hilsim_return_log diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index 2bd3d38..04895dc 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -9,40 +9,99 @@ # will be to recieve job data and pass it on to whichever device is connected to this device, then send that data back. # That's it! Work of when to run jobs/hardware cooldowns/getting jobs will be handled by the central server. -import av_platform.run_setup as run -import util.git_commands as git -import util.pio_commands as pio -import util.serial_wrapper as s +import av_platform.interface as avionics import av_platform.stream_data as platform +import util.serial_wrapper as server +import util.packets as packet +import util.config as config import os import time +import serial ############### HILSIM Data-Streamer-RASPI ############### -def getfirstport(): - if(len(s.connected_comports) > 0): - return s.connected_comports[0] - else: - print("didn't find any ports.") - return False +server_port: serial.Serial = None +tester_boards: list[serial.Serial] = None +board_id = -1 + +def ready(): + return avionics.ready and server_port != None def main(): + global server_port, tester_boards, board_id # TODO: Implement application-specific setup # TODO: Implement stack-specific setup # TODO: Implement Server-application communication (Serial communication) # TODO: Testing? - git.remote_clone() - git.remote_reset() - pio.pio_clean() - git.remote_pull_branch("AV-999/protobuf-integration") + # Initialize repository + # avionics.first_setup() + + next_conn_debounce = time.time() + 0.5 + next_av_debounce = time.time() + 0.1 + + print("(datastreamer) first setup done") + while True: # Run setup + # Are we in init state? (Don't have a server connected) + if server_port == None and time.time() > next_conn_debounce: + print("(datastreamer) Missing server connection! Running server probe") + server.t_init_com_ports() + for port in server.connected_comports: + server.send_ident(port) + + next_conn_debounce = time.time() + 0.5 + + + if not avionics.ready and time.time() > next_av_debounce: + print("(datastreamer) Attempting to detect avionics") + avionics.detect_avionics([server_port]) + # let's just assume avionics is ready + avionics.ready = True + if(avionics.ready): + print("(datastreamer) Avionics ready!") + next_av_debounce = time.time() + 0.1 + + listener_list = server.connected_comports + if(server_port != None): + listener_list = [server_port] + # Process all incoming packets + for comport in listener_list: + if comport.in_waiting: + data = port.read_all() + packet_string = data.decode("utf8") + valid, type, data = packet.decode_packet(packet_string) + if not valid: + comport.write(packet.construct_invalid(packet_string).encode()) + else: + if type == "ACK": + if server_port == None: + server_port = comport + board_id = data['board_id'] + print("(datastreamer) Connected to server!") + continue + else: + print("Recieved ACK packets from more than one source! Discarding second ACK.") + comport.write(packet.construct_invalid(packet_string).encode()) + continue - try: - pio.pio_upload("mcu_hilsim") - except: - pio.pio_upload("mcu_hilsim") + if (not ready()): + print("(datastreamer) Recieved packet but not ready! Send ACK first.") + comport.write(packet.construct_invalid(packet_string).encode()) + continue + + match type: + case "IDENT?": + comport.write(packet.construct_id_confirm(config.board_type, board_id).encode()) + + + + + + + + print("(main) Waiting 5s for port to open back up") time.sleep(5) # Wait for port to start back up diff --git a/Test-Rack-Software/TARS-Rack/sv_pkt.py b/Test-Rack-Software/TARS-Rack/sv_pkt.py new file mode 100644 index 0000000..4d36134 --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/sv_pkt.py @@ -0,0 +1,68 @@ +# Contains all communication packets required for the SERVER side of the server-datastreamer communication. +# Contains constructor functions for each packet and also a decode function for client packets. +import json + +#### SERVER PACKETS #### +def construct_ident_probe(): + # Constructs IDENT? packet + packet_dict = {'type': "IDENT?", 'data': {}} + return json.dumps(packet_dict) + +def construct_ping(): + # Constructs PING packet + packet_dict = {'type': "PING", 'data': {}} + return json.dumps(packet_dict) + +def construct_acknowledge(board_id: int): + # Constructs ACK packet + packet_dict = {'type': "ACK", 'data': {'board_id': board_id}} + return json.dumps(packet_dict) + +def construct_reassign(board_id: int): + # Constructs ACK packet + packet_dict = {'type': "REASSIGN", 'data': {'board_id': board_id}} + return json.dumps(packet_dict) + +def construct_terminate(): + # Constructs TERMINATE packet + packet_dict = {'type': "TERMINATE", 'data': {}} + return json.dumps(packet_dict) + +def construct_cycle(): + # Constructs TERMINATE packet + packet_dict = {'type': "CYCLE", 'data': {}} + return json.dumps(packet_dict) + +def construct_job(job_data, flight_csv): + # Constructs TERMINATE packet + packet_dict = {'type': "JOB", 'data': {'job_data': job_data, 'sim_data': flight_csv}} + return json.dumps(packet_dict) + +#### CLIENT PACKETS #### +def decode_packet(packet: str): + packet_dict = json.loads(packet) + if(type(packet_dict) == str): + packet_dict = json.loads(packet_dict) + return validate_client_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] + +def validate_client_packet(packet_type: str, packet_data): + match packet_type: + case "IDENT": + return "board_type" in packet_data + case "ID-CONF": + return "board_id" in packet_data and "board_type" in packet_data + case "READY": + return True + case "DONE": + return "hilsim_result" in packet_data and "job_data" in packet_data + case "JOB-UPD": + return "hilsim_result" in packet_data and "job_status" in packet_data + case "INVALID": + return "raw_packet" in packet_data + case "BUSY": + return "job_data" in packet_data + case "PONG": + return True + + + \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/test_server_delete.py b/Test-Rack-Software/TARS-Rack/test_server_delete.py new file mode 100644 index 0000000..37c8545 --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/test_server_delete.py @@ -0,0 +1,32 @@ +# DELETE THIS! + +import serial +import sys +import sv_pkt as packet + +argc = len(sys.argv) + +comport = "COM9" +port = serial.Serial(comport, write_timeout=1.0) + +if(argc == 1): + print("Needs a packet ") + exit(1) + +pkt = sys.argv[1] + +match pkt: + case "ACK": + port.write(packet.construct_acknowledge(0).encode()) + case "IDENT?": + port.write(packet.construct_ident_probe().encode()) + +print("Sent") +while True: + if port.in_waiting: + data = port.read_all() + packet_string = data.decode("utf8") + print(packet_string) + + + diff --git a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py index 4f82452..483b8b3 100644 --- a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py +++ b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py @@ -3,7 +3,7 @@ import util.config import util.packets as packet -connected_comports = [] +connected_comports: list[serial.Serial] = [] """ Function to retrieve a list of comports connected to this device @@ -13,18 +13,40 @@ def get_com_ports(): return serial.tools.list_ports.comports() +"""Close all connected COMports""" +def close_com_ports(): + print("(cloase_com_ports) Closing all initialized comports..") + for port in connected_comports: + port.close() + +def close_port(port: serial.Serial): + print("(cloase_com_ports) Closing " + port.name) + for comport in connected_comports: + if(comport.name == port.name): + comport.close() + +""" +TODO: Delete this! +Test script for init_com_ports() """ -Close all initialized ports, then loop through each port and initialize it. +def t_init_com_ports(): + print("(init_comports) Attempting to initialize all connected COM ports..") + if(len(connected_comports) > 0): + return + port = serial.Serial("COM8", write_timeout=0) + connected_comports.append(port) + print("(init_comports) Initialized port COM8") + +""" +Loop through each port and try to initialize it if it's not already initialized """ def init_com_ports(): - print("(init_comports) Closing all initialized comports..") - for port in connected_comports: - port.close() print("(init_comports) Attempting to initialize all connected COM ports..") for port_data in get_com_ports(): try: - port = serial.Serial(port_data.device) + port = serial.Serial(port_data.device, write_timeout=0) connected_comports.append(port) + print("(init_comports) Initialized port " + port_data.device) except serial.SerialException as err: if("denied" in str(err)): for connected in connected_comports: @@ -43,7 +65,7 @@ def init_com_ports(): @param port: serial.Serial -- Port to send the IDENT packet to """ def send_ident(port: serial.Serial): - port.write(packet.construct_ident(config.boa)) + port.write(packet.construct_ident(util.config.board_type).encode()) """ The main function that handles all packets sent by the server, also returns the packet data to the caller. From a2a5ac387d456588e2906bd0d8f69b23280d1952 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Sat, 7 Oct 2023 18:49:45 -0500 Subject: [PATCH 07/24] Datastreamer work --- .../TARS-Rack/av_platform/interface.py | 85 +++++++-- Test-Rack-Software/TARS-Rack/main.py | 164 ++++++++++++++---- Test-Rack-Software/TARS-Rack/sv_pkt.py | 4 +- .../TARS-Rack/test_server_delete.py | 22 +-- Test-Rack-Software/TARS-Rack/util/packets.py | 19 +- .../TARS-Rack/util/serial_wrapper.py | 22 ++- 6 files changed, 241 insertions(+), 75 deletions(-) diff --git a/Test-Rack-Software/TARS-Rack/av_platform/interface.py b/Test-Rack-Software/TARS-Rack/av_platform/interface.py index 2240124..b78f7ef 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/interface.py +++ b/Test-Rack-Software/TARS-Rack/av_platform/interface.py @@ -1,6 +1,7 @@ # AV Interface (TARS) # This is the specific interface.py file for the TARS avionics stack. -# @implements +# @implements handle_raw(str raw_string) -- Determines what to do when a raw string is recieved from the stack. +# @implements detect_avionics(serial.Serial[] ignorePorts) -- Scans all ports for a connection, sets ready status if connected to an avionics stack. # @implements first_setup() -- Performs the first setup (assuming fresh install). # @implements code_reset() -- Resets the project to a "known" state (Usually master branch). # @implements code_pull(str git_branch) -- Sets up the project to do a code flash. @@ -20,15 +21,25 @@ ready = False # Is the stack ready to recieve data? TARS_port: serial.Serial = None +"""This function handles all raw input BEFORE all initialization is complete.""" +# Doesn't do anything for TARS, but other boards may have initialization packets. +def handle_raw(raw_string: str): + pass + """This function will check if there's any serial plugged into this board that is NOT the server. TARS only has one board, so we're good if we see any other port""" -def detect_avionics(ignore_ports: list[serial.Serial]): +def detect_avionics(ignore_ports: list[serial.Serial], connected_to_server: bool): + global ready, TARS_port + # For TARS, we need to make sure that we're already connected to the server + if(not connected_to_server): + ready = False + return + print("(detect_avionics) Attempting to detect avionics") for comport in server.connected_comports: if not (comport in ignore_ports): - if(comport.in_waiting): - TARS_port = comport - ready = True - ready = False + print("(detect_avionics) Detected viable target @ " + comport.name) + TARS_port = comport + ready = True """ @@ -46,6 +57,8 @@ def first_setup(): """ def code_reset(): git.remote_reset() + # Clean build dir + pio.pio_clean() """ This function must be implemented in all run_setup.py functions for each stack @@ -60,8 +73,6 @@ def code_pull(git_branch: str): TARS: Uses environment mcu_hilsim """ def code_flash(): - # Clean build dir - pio.pio_clean() # For TARS, we need to attempt the code flash twice, since it always fails the first time. try: pio.pio_upload("mcu_hilsim") @@ -83,9 +94,51 @@ class HilsimRun: start_time = 0 current_time = 0 port = None + job_data = None + + # Getter for current log + def get_current_log(self): + return self.return_log + + # Sets up job to run (Cannot be canceled) + # @param cancel_callback: Function that returns whether the job should be terminated. + def job_setup(self, cancel_callback): + job = self.job_data + # Temporarily close port so code can flash + TARS_port.close() + if(job['pull_type'] == "branch"): + try: + code_reset() + if(cancel_callback()): + return False, "Terminate signal sent during setup" + code_pull(job['pull_target']) + if(cancel_callback()): + return False, "Terminate signal sent during setup" + code_flash() + + # Wait for the port to open back up (Max wait 10s) + start = time.time() + while(time.time() < start + 10): + if(cancel_callback()): + return False, "Terminate signal sent during COMPort setup" + try: + TARS_port.open() + print("\n(job_setup) Successfully re-opened TARS port (" + TARS_port.name + ")") + return True, "Setup Complete" + except: + time_left = abs((start + 10) - time.time()) + print(f"(job_setup) attempting to re-open tars port.. ({time_left:.1f}s)", end="\r") + return False, "Unable to re-open avionics COM Port" + + except Exception as e: + return False, "Setup failed: " + str(e) + elif (job['pull_type'] == "commit"): + # Not implemented yet + pass + # Turns a raw CSV string to a Pandas dataframe - def raw_csv_to_dataframe(raw_csv) -> pandas.DataFrame: + def raw_csv_to_dataframe(self, raw_csv) -> pandas.DataFrame: # Get column names header = raw_csv.split('\n')[0].split(",") csv = "\n".join(raw_csv.split('\n')[1:]) @@ -93,14 +146,16 @@ def raw_csv_to_dataframe(raw_csv) -> pandas.DataFrame: return pandas.read_csv(csvStringIO, sep=",", header=None, names=header) # Initializes the HILSIM run object - def __init__(self, raw_csv: str, serial_port: serial.Serial) -> None: + def __init__(self, raw_csv: str, job: dict) -> None: + global TARS_port self.flight_data_raw = raw_csv self.flight_data_dataframe = self.raw_csv_to_dataframe(self.flight_data_raw) self.flight_data_rows = self.flight_data_dataframe.iterrows() - self.port = serial_port + self.port = TARS_port self.start_time = time.time() self.current_time = self.start_time self.last_packet_time = self.start_time + self.job_data = job """ Runs one iteration of the HILSIM loop, with a change in time of dt. @@ -110,8 +165,8 @@ def __init__(self, raw_csv: str, serial_port: serial.Serial) -> None: """ def step(self, dt: float, callback_func): self.current_time += dt - if self.current_time > self.last_packet_time + 10: - self.last_packet_time += 10 + if self.current_time > self.last_packet_time + 1: + self.last_packet_time += 0.01 if self.current_time < self.start_time + 5: # Wait for 5 seconds to make sure serial is connected pass @@ -119,7 +174,7 @@ def step(self, dt: float, callback_func): if(self.current_line == 0): callback_func(packet.construct_job_status(True, "running", f"Running (Data streaming started)")) self.current_line += 1 - + if(self.current_line % 300 == 0): # Only send a job update every 3-ish seconds callback_func(packet.construct_job_status(True, "running", f"Running ({self.current_line/len(self.flight_data_dataframe)*100:.2f}%) [{self.current_line} processed out of {len(self.flight_data_dataframe)} total]")) @@ -144,6 +199,8 @@ def step(self, dt: float, callback_func): if string: string = string[0 : (len(string)-1)] self.return_log.append(string) + + return False, False, self.return_log diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index 04895dc..1a29deb 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -23,12 +23,105 @@ server_port: serial.Serial = None tester_boards: list[serial.Serial] = None board_id = -1 +current_job: avionics.HilsimRun = None +current_job_data: dict = None +signal_abort = False +job_active = False def ready(): return avionics.ready and server_port != None +def setup_job(job_packet_data): + global current_job + current_job = avionics.HilsimRun(job_packet_data['csv_data'], job_packet_data['job_data']) + current_job_data = job_packet_data['job_data'] + +def handle_server_packet(packet_type, packet_data, packet_string, comport: serial.Serial): + global server_port, job_active + if(packet_type != "JOB"): + print("Handling packet << " + packet_string) + else: + print("Handling packet << " + packet_type + f" ({packet_data['job_data']}) + [hidden csv data]") + global server_port, board_id + if packet_type == "ACK": + if server_port == None: + server_port = comport + board_id = packet_data['board_id'] + print("(datastreamer) Connected to server as board " + str(board_id) + "!") + return + else: + print("Recieved ACK packets from more than one source! Discarding second ACK.") + comport.write(packet.construct_invalid(packet_string).encode()) + return + + if (not ready()): + print("(datastreamer) Recieved packet but not ready! Send ACK first.") + comport.write(packet.construct_invalid(packet_string).encode()) + return + log_string = "" + match packet_type: + case "IDENT?": + comport.write(packet.construct_id_confirm(config.board_type, board_id).encode()) + log_string += "Sending packet >> " + packet.construct_id_confirm(config.board_type, board_id) + case "PING": + comport.write(packet.construct_pong().encode()) + log_string += "Sending packet >> " + packet.construct_pong() + case "REASSIGN": + if(board_id != -1): + new_id = packet_data['board_id'] + log_string += "Reassigned board ID (" + str(board_id) + " ==> " + str(new_id) + ")" + board_id = new_id + else: + comport.write(packet.construct_invalid(packet_string).encode()) + log_string += "Recieved REASSIGN packet but this board has not been initialized" + case "JOB": + try: + setup_job(packet_data) + job_status = packet.construct_job_status(True, "Setup", "Accepted") + log_string += "Job set up" + print("(handle_packet) Job accepted") + comport.write(packet.construct_job_update(job_status, []).encode()) + + # Returns true if the job should be canceled. + def cancel_job(): + read_data([server_port]) + return signal_abort + + completed, status = current_job.job_setup(cancel_job) + job_status = packet.construct_job_status(completed, "Setup", status) + comport.write(packet.construct_job_update(job_status, []).encode()) + + if(completed): + job_active = True + except Exception as e: + log_string += "Job rejected: " + str(e) + comport.write(packet.construct_invalid(packet_string).encode()) + if(log_string == ""): + log_string = "(handle_packet) No special log string for pkt\n" + packet_string + else: + log_string = "(handle_packet) " + log_string + print(log_string) + +def read_data(listener_list: list[serial.Serial]): + # Process all incoming packets (Caution! Will process EVERYTHING before server is connected!) + for comport in listener_list: + if comport.in_waiting: + data = comport.read_all() + packet_string = data.decode("utf8") + valid, type, data = packet.decode_packet(packet_string) + if not valid: + # If it's not valid, it's either not a valid packet, or type="RAW" (just a string) + if type == "RAW": + avionics.handle_raw(packet_string) + else: + # invalid packet + comport.write(packet.construct_invalid(packet_string).encode()) + else: + # Is server packet + handle_server_packet(type, data, packet_string, comport) + def main(): - global server_port, tester_boards, board_id + global server_port, tester_boards, board_id, job_active # TODO: Implement application-specific setup # TODO: Implement stack-specific setup # TODO: Implement Server-application communication (Serial communication) @@ -39,6 +132,7 @@ def main(): next_conn_debounce = time.time() + 0.5 next_av_debounce = time.time() + 0.1 + last_job_loop_time = time.time() print("(datastreamer) first setup done") while True: # Run setup @@ -53,10 +147,11 @@ def main(): if not avionics.ready and time.time() > next_av_debounce: - print("(datastreamer) Attempting to detect avionics") - avionics.detect_avionics([server_port]) - # let's just assume avionics is ready - avionics.ready = True + avionics.detect_avionics([server_port], server_port != None) + + # Uncomment line below if testing with actual avionics + # avionics.ready = True + if(avionics.ready): print("(datastreamer) Avionics ready!") next_av_debounce = time.time() + 0.1 @@ -65,35 +160,36 @@ def main(): if(server_port != None): listener_list = [server_port] - # Process all incoming packets - for comport in listener_list: - if comport.in_waiting: - data = port.read_all() - packet_string = data.decode("utf8") - valid, type, data = packet.decode_packet(packet_string) - if not valid: - comport.write(packet.construct_invalid(packet_string).encode()) - else: - if type == "ACK": - if server_port == None: - server_port = comport - board_id = data['board_id'] - print("(datastreamer) Connected to server!") - continue - else: - print("Recieved ACK packets from more than one source! Discarding second ACK.") - comport.write(packet.construct_invalid(packet_string).encode()) - continue - - - if (not ready()): - print("(datastreamer) Recieved packet but not ready! Send ACK first.") - comport.write(packet.construct_invalid(packet_string).encode()) - continue - - match type: - case "IDENT?": - comport.write(packet.construct_id_confirm(config.board_type, board_id).encode()) + # Reads all comport data and acts on it + read_data(listener_list) + + def send_running_job_status(job_status_packet): + server_port.write(packet.construct_job_update(job_status_packet, current_job.get_current_log()).encode()) + print(f"(current_job_status) Is_OK: {str(job_status_packet['job_ok'])}, Current action: {job_status_packet['current_action']}, Status: {job_status_packet['status']}") + + # Runs currently active job (If any) + if(job_active): + if(last_job_loop_time != time.time()): + dt = time.time() - last_job_loop_time + run_finished, run_errored, cur_log = current_job.step(dt, send_running_job_status) + + + # CURRENT ISSUE: + # Doesn't stream enough data. + + + + if(run_finished): + job_active = False + pass + + if(run_errored): + job_active = False + pass + + last_job_loop_time = time.time() + + diff --git a/Test-Rack-Software/TARS-Rack/sv_pkt.py b/Test-Rack-Software/TARS-Rack/sv_pkt.py index 4d36134..f680019 100644 --- a/Test-Rack-Software/TARS-Rack/sv_pkt.py +++ b/Test-Rack-Software/TARS-Rack/sv_pkt.py @@ -39,10 +39,8 @@ def construct_job(job_data, flight_csv): return json.dumps(packet_dict) #### CLIENT PACKETS #### -def decode_packet(packet: str): +def decode_packet(packet: str) -> tuple[bool, str, dict]: packet_dict = json.loads(packet) - if(type(packet_dict) == str): - packet_dict = json.loads(packet_dict) return validate_client_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] def validate_client_packet(packet_type: str, packet_data): diff --git a/Test-Rack-Software/TARS-Rack/test_server_delete.py b/Test-Rack-Software/TARS-Rack/test_server_delete.py index 37c8545..75b44e6 100644 --- a/Test-Rack-Software/TARS-Rack/test_server_delete.py +++ b/Test-Rack-Software/TARS-Rack/test_server_delete.py @@ -3,30 +3,26 @@ import serial import sys import sv_pkt as packet +import time argc = len(sys.argv) comport = "COM9" port = serial.Serial(comport, write_timeout=1.0) -if(argc == 1): - print("Needs a packet ") - exit(1) +print("Init") +start = time.time() +delay_until_ack = 0.5; ack_run = False +delay_until_ident = 1; ident_run = False -pkt = sys.argv[1] - -match pkt: - case "ACK": - port.write(packet.construct_acknowledge(0).encode()) - case "IDENT?": - port.write(packet.construct_ident_probe().encode()) - -print("Sent") while True: if port.in_waiting: data = port.read_all() packet_string = data.decode("utf8") - print(packet_string) + valid, p_type, data = packet.decode_packet(packet_string) + + print( type(data) ) + print(valid, p_type, data) diff --git a/Test-Rack-Software/TARS-Rack/util/packets.py b/Test-Rack-Software/TARS-Rack/util/packets.py index 56edaaa..9c7e427 100644 --- a/Test-Rack-Software/TARS-Rack/util/packets.py +++ b/Test-Rack-Software/TARS-Rack/util/packets.py @@ -34,7 +34,7 @@ def construct_busy(job_data): packet_dict = {'type': "BUSY", 'data': {'job_data': job_data}} return json.dumps(packet_dict) -def construct_job_update(job_status, current_log: str): +def construct_job_update(job_status, current_log: list[str]): # Constructs JOB-UPD packet packet_dict = {'type': "JOB-UPD", 'data': {'job_status': job_status, 'hilsim_result': current_log}} return json.dumps(packet_dict) @@ -45,12 +45,23 @@ def construct_pong(): return json.dumps(packet_dict) #### Intermediate data #### -def construct_job_status(job_ok, current_action, status_text): +def construct_job_status(job_ok: bool, current_action: str, status_text: str): return {"job_ok": job_ok, 'current_action': current_action, "status": status_text} #### SERVER PACKETS #### def decode_packet(packet: str): - packet_dict = json.loads(packet) + try: + if "[raw==>]" in packet: + # This is a job packet, we treat it differently. + packet_split = packet.split("[raw==>]") + job_packet = packet_split[0] + raw_data = packet_split[1] + packet_dict = json.loads(job_packet) + packet_dict['data']['csv_data'] = raw_data + return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] + packet_dict = json.loads(packet) + except Exception as e: + return False, "RAW", packet return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] def validate_server_packet(packet_type: str, packet_data): @@ -67,5 +78,7 @@ def validate_server_packet(packet_type: str, packet_data): return True case "JOB": return "job_data" in packet_data and "csv_data" in packet_data + case "PING": + return True \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py index 483b8b3..4fa4fba 100644 --- a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py +++ b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py @@ -25,17 +25,25 @@ def close_port(port: serial.Serial): if(comport.name == port.name): comport.close() +"""Clears all of the data in the port buffer""" +def clear_port(port: serial.Serial): + if port.in_waiting: + data = port.read_all() + """ TODO: Delete this! Test script for init_com_ports() """ +alr_init = False def t_init_com_ports(): - print("(init_comports) Attempting to initialize all connected COM ports..") - if(len(connected_comports) > 0): - return - port = serial.Serial("COM8", write_timeout=0) - connected_comports.append(port) - print("(init_comports) Initialized port COM8") + global alr_init + init_com_ports() + if alr_init == False: + port = serial.Serial("COM8", write_timeout=0) + connected_comports.append(port) + alr_init = True + print("(init_comports) Initialized port COM8") + """ Loop through each port and try to initialize it if it's not already initialized @@ -52,8 +60,6 @@ def init_com_ports(): for connected in connected_comports: if(connected.name == port_data.device): print("(init_comports) " + port_data.device + " already initialized") - else: - print("(init_comports) " + port_data.device + " cannot be initialized.") else: print(err) From a66c11f3515788b326905f05c2236939f8578a57 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Sun, 8 Oct 2023 16:22:13 -0500 Subject: [PATCH 08/24] Testing revert of [raw==>] workaround --- .../API/services/datastreamer_test.py | 91 +- .../API/services/datastreamer_test_data.csv | 800 ++++++++++++++++++ Central-Server/API/services/serial_tester.py | 15 +- Central-Server/API/util/client_packets.py | 9 + Central-Server/API/util/packets.py | 4 +- .../TARS-Rack/av_platform/interface.py | 12 +- Test-Rack-Software/TARS-Rack/main.py | 24 +- .../TARS-Rack/util/serial_wrapper.py | 16 +- 8 files changed, 928 insertions(+), 43 deletions(-) create mode 100644 Central-Server/API/services/datastreamer_test_data.csv diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index 4436e4d..8b95783 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -6,42 +6,29 @@ import sys import serial import os +import time +import json sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) import util.packets as pkt +comport = "COM9" +if __name__ == "__main__": -comport = "COM9" -if __name__ == "__main__": print("Detected script running as __main__, beginning test of Data Streamer functionality") serial_tester.SECTION("Test setup") port = serial_tester.TRY_OPEN_PORT(comport) - - # Setup - serial_tester.RESET_TEST(port) - - # > PING - serial_tester.SECTION("PING - Signal testing") - serial_tester.TRY_WRITE(port, pkt.construct_ping().encode(), "Writing PING packet") - serial_tester.TEST("Responds to PING packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, "PONG", valid, type, data)) - - # > IDENT? - serial_tester.SECTION("IDENT? - Identity confirmation") - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet") - serial_tester.TEST("Responds to IDENT? packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + serial_tester.TEST("Waiting for any packet..", serial_tester.AWAIT_ANY_RESPONSE(port, 100, "Run the datastreamer script")) # > ACK 1 ack_test_boardid = 4 serial_tester.SECTION("[1/2] ACK - Valid Acknowledge packet") + serial_tester.RESET_TEST(port) serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing valid ACK packet") serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after ACK packet") @@ -56,6 +43,22 @@ res = "ID-CONF correctly returned board ID " + str(ack_test_boardid) serial_tester.TEST("Connected board returns properly set ID", (cond, res)) + + # > PING + serial_tester.SECTION("PING - Signal testing") + + serial_tester.TRY_WRITE(port, pkt.construct_ping().encode(), "Writing PING packet") + serial_tester.TEST("Responds to PING packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, "PONG", valid, type, data)) + + # > IDENT? + serial_tester.SECTION("IDENT? - Identity confirmation") + serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet") + serial_tester.TEST("Responds to IDENT? packet", serial_tester.AWAIT_ANY_RESPONSE(port)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + # > ACK 2 (invalid) ack_test_boardid = 0 serial_tester.SECTION("[2/2] ACK - Acknowledge packet after ACK") @@ -80,13 +83,51 @@ res = "ID-CONF correctly returned board ID " + str(reassign_test_boardid) serial_tester.TEST("Connected board returns properly set ID", (cond, res)) - # > REASSIGN 2 - reassign_test_boardid = 8 - serial_tester.RESET_TEST(port) - serial_tester.SECTION("REASSIGN - [Invalid] Assign new board ID to fresh rack") - serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing invalid REASSIGN packet") + # Jobs + serial_tester.SECTION("Comprehensive JOB Tests") + serial_tester.SECTION("Initializes job and sends updates") + + # Get job data + + job_data = {"pull_type": "branch", "pull_target": "master", "job_type": "default", + "job_author_id": "github_id_here", "priority": 0, "timestep": 1} + + # Open csv file + file = open(os.path.join(os.path.dirname(__file__), "./datastreamer_test_data.csv"), 'r') + csv_data = file.read() + + serial_tester.TRY_WRITE(port, pkt.construct_job(job_data, csv_data).encode(), "Writing JOB packet") + serial_tester.TEST("Processes valid JOB packet and responds.. (extended time)", serial_tester.AWAIT_ANY_RESPONSE(port, 10)) + + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Packet after valid JOB packet complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) + job_good = data['job_status']['job_ok'] == True and data['job_status']['status'] == "Accepted" + serial_tester.TEST("Ensure job_ok is True and job_status is 'Accepted'", (job_good, f"Got job_ok {data['job_status']['job_ok']} and job_status '{data['job_status']['status']}'")) + serial_tester.TEST("Responds after building job", serial_tester.AWAIT_ANY_RESPONSE(port, 100, "(Waiting for build: This will take a while.)")) + + # Check for ok update after flash + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Packet after expected build complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) + job_good = data['job_status']['job_ok'] == True and data['job_status']['status'] == "Setup Complete" + serial_tester.TEST("Ensure job_ok is True and job_status is 'Setup Complete'", (job_good, f"Got job_ok {data['job_status']['job_ok']} and job_status '{data['job_status']['status']}'")) + + # Job updates + serial_tester.TEST("Job updates are sent", serial_tester.AWAIT_ANY_RESPONSE(port, 10, "(Waiting for any job update)")) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Packet after expected run start complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) + job_good = data['job_status']['job_ok'] == True + serial_tester.TEST("Ensure job_ok is True", (job_good, f"Got job_ok {data['job_status']['job_ok']}")) + + # Terminate + time.sleep(0.5) + serial_tester.TRY_WRITE(port, pkt.construct_terminate().encode(), "Writing TERMINATE packet") + serial_tester.TEST("TERMINATE response sent (job packet)", serial_tester.AWAIT_ANY_RESPONSE(port, 3, "(Waiting for job update)")) valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("INVALID packet after non-initialized REASSIGN packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) + + print(valid,type,data) + + + # CLEANUP serial_tester.SECTION("Cleanup") diff --git a/Central-Server/API/services/datastreamer_test_data.csv b/Central-Server/API/services/datastreamer_test_data.csv new file mode 100644 index 0000000..48a8f7a --- /dev/null +++ b/Central-Server/API/services/datastreamer_test_data.csv @@ -0,0 +1,800 @@ +timestamp_ms,ax,ay,az,gx,gy,gz,mx,my,mz,latitude,longitude,altitude,satellite_count,position_lock,temperature,pressure,barometer_altitude,highg_ax,highg_ay,highg_az,rocket_state0,rocket_state1,rocket_state2,rocket_state3,flap_extension,state_est_x,state_est_vx,state_est_ax,state_est_apo,battery_voltage +1945435,0.014091,-0.025071,1.04365,-1.96875,1.19,-22.1375,0.3759,0.11578,-0.52976,41.4877,-89.5063,181.342,13,1,33.38,1025.95,-112.393,0.09375,0.0957031,1.05469,1,1,1,1,0,-111.919,0.215788,0.0288778,-112.006,8.03408 +1945445,0.014091,-0.025071,1.04365,-1.96875,1.19,-22.1375,0.3759,0.11578,-0.52976,41.4877,-89.5063,181.342,13,1,33.38,1025.95,-112.393,0.09375,0.0957031,1.05469,1,1,1,1,0,-111.919,0.215788,0.0288778,-112.006,8.03408 +1945455,0.014091,-0.025071,1.04365,-1.96875,1.19,-22.1375,0.3759,0.11578,-0.52976,41.4877,-89.5063,181.342,13,1,33.38,1025.95,-112.393,0.09375,0.0957031,1.05469,1,1,1,1,0,-111.919,0.215788,0.0288778,-112.006,8.03408 +1945465,0.02013,-0.02257,1.02614,-1.12,1.1025,-22.1025,0.37548,0.1141,-0.52556,41.4877,-89.5063,181.342,13,1,33.38,1025.94,-112.305,0.0917969,0.09375,1.05469,1,1,1,1,0,-111.919,0.215788,0.0288778,-112.006,8.01475 +1945475,0.02013,-0.02257,1.02614,-1.12,1.1025,-22.1025,0.37548,0.1141,-0.52556,41.4877,-89.5063,181.342,13,1,33.38,1025.94,-112.305,0.0917969,0.09375,1.05469,1,1,1,1,0,-111.919,0.215788,0.0288778,-112.006,8.01475 +1945485,0.02013,-0.02257,1.02614,-1.12,1.1025,-22.1025,0.37548,0.1141,-0.52556,41.4877,-89.5063,181.342,13,1,33.38,1025.94,-112.305,0.0917969,0.09375,1.05469,1,1,1,1,0,-111.919,0.215788,0.0288778,-112.006,8.01475 +1945495,0.012322,-0.019703,1.04218,-4.71625,5.355,-1.75875,0.39424,0.17962,-0.50568,41.4877,-89.5063,181.342,13,1,33.38,1025.93,-112.217,0.0898438,0.0917969,1.05469,1,1,1,1,0,-111.917,0.215725,0.026739,-112.002,8.04375 +1945505,0.012322,-0.019703,1.04218,-4.71625,5.355,-1.75875,0.39424,0.17962,-0.50568,41.4877,-89.5063,181.342,13,1,33.38,1025.93,-112.217,0.0898438,0.0917969,1.05469,1,1,1,1,0,-111.917,0.215725,0.026739,-112.002,8.04375 +1945515,0.010004,-0.0305,1.03011,-1.365,2.28375,-19.4513,0.38696,0.13734,-0.5327,41.4877,-89.5063,181.342,13,1,33.38,1025.95,-112.393,0.0859375,0.0898438,1.05469,1,1,1,1,0,-111.917,0.215725,0.026739,-112.002,8.04375 +1945525,0.010004,-0.0305,1.03011,-1.365,2.28375,-19.4513,0.38696,0.13734,-0.5327,41.4877,-89.5063,181.342,13,1,33.38,1025.95,-112.393,0.0859375,0.0898438,1.05469,1,1,1,1,0,-111.917,0.215725,0.026739,-112.002,8.04375 +1945535,0.010004,-0.0305,1.03011,-1.365,2.28375,-19.4513,0.38696,0.13734,-0.5327,41.4877,-89.5063,181.342,13,1,33.38,1025.95,-112.393,0.0859375,0.0898438,1.05469,1,1,1,1,0,-111.917,0.215725,0.026739,-112.002,8.04375 +1945545,0.010187,-0.042944,1.032,-2.9225,2.1875,-11.0337,0.36736,0.10948,-0.50666,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0898438,0.0976562,1.05469,1,1,1,1,0,-111.912,0.21616,0.0265023,-112,8.00508 +1945555,0.010187,-0.042944,1.032,-2.9225,2.1875,-11.0337,0.36736,0.10948,-0.50666,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0898438,0.0976562,1.05469,1,1,1,1,0,-111.912,0.21616,0.0265023,-112,8.00508 +1945565,0.008052,-0.030561,1.02901,-3.84125,5.80125,-13.4488,0.39592,0.10766,-0.50652,41.4877,-89.5063,181.347,13,1,33.39,1025.93,-112.221,0.0898438,0.0976562,1.05469,1,1,1,1,0,-111.912,0.21616,0.0265023,-112,8.00508 +1945575,0.008052,-0.030561,1.02901,-3.84125,5.80125,-13.4488,0.39592,0.10766,-0.50652,41.4877,-89.5063,181.347,13,1,33.39,1025.93,-112.221,0.09375,0.101562,1.05469,1,1,1,1,0,-111.912,0.21616,0.0265023,-112,8.03408 +1945585,0.008052,-0.030561,1.02901,-3.84125,5.80125,-13.4488,0.39592,0.10766,-0.50652,41.4877,-89.5063,181.347,13,1,33.39,1025.93,-112.221,0.09375,0.101562,1.05469,1,1,1,1,0,-111.912,0.21616,0.0265023,-112,8.03408 +1945595,0.014701,-0.028975,1.04524,-3.85875,-0.63875,-0.3675,0.36848,0.10528,-0.5544,41.4877,-89.5063,181.347,13,1,33.39,1025.96,-112.484,0.09375,0.101562,1.05273,1,1,1,1,0,-111.91,0.215557,0.00949148,-111.994,8.01475 +1945605,0.014701,-0.028975,1.04524,-3.85875,-0.63875,-0.3675,0.36848,0.10528,-0.5544,41.4877,-89.5063,181.347,13,1,33.39,1025.96,-112.484,0.09375,0.101562,1.05273,1,1,1,1,0,-111.91,0.215557,0.00949148,-111.994,8.01475 +1945615,0.014701,-0.028975,1.04524,-3.85875,-0.63875,-0.3675,0.36848,0.10528,-0.5544,41.4877,-89.5063,181.347,13,1,33.39,1025.96,-112.484,0.09375,0.101562,1.05273,1,1,1,1,0,-111.91,0.215557,0.00949148,-111.994,8.01475 +1945625,0.000427,-0.012627,1.03236,-3.31625,1.68,-8.575,0.36218,0.10808,-0.51408,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0878906,0.0976562,1.05273,1,1,1,1,0,-111.91,0.215557,0.00949148,-111.994,8.04375 +1945635,0.000427,-0.012627,1.03236,-3.31625,1.68,-8.575,0.36218,0.10808,-0.51408,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0878906,0.0976562,1.05273,1,1,1,1,0,-111.91,0.215557,0.00949148,-111.994,8.04375 +1945645,0.003599,-0.028853,1.04084,-3.57875,2.14375,-6.9825,0.39914,0.19068,-0.56308,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0859375,0.109375,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945655,0.003599,-0.028853,1.04084,-3.57875,2.14375,-6.9825,0.39914,0.19068,-0.56308,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0859375,0.109375,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945665,0.003599,-0.028853,1.04084,-3.57875,2.14375,-6.9825,0.39914,0.19068,-0.56308,41.4877,-89.5063,181.347,13,1,33.39,1025.95,-112.396,0.0859375,0.109375,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945675,0.007625,-0.018971,1.03529,-3.77125,1.98625,-5.5475,0.3724,0.112,-0.54236,41.4877,-89.5063,181.347,13,1,33.37,1025.94,-112.301,0.0859375,0.109375,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945685,0.007625,-0.018971,1.03529,-3.77125,1.98625,-5.5475,0.3724,0.112,-0.54236,41.4877,-89.5063,181.347,13,1,33.37,1025.94,-112.301,0.0859375,0.109375,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945695,0.007625,-0.018971,1.03529,-3.77125,1.98625,-5.5475,0.3724,0.112,-0.54236,41.4877,-89.5063,181.347,13,1,33.37,1025.94,-112.301,0.0839844,0.113281,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945705,0.007625,-0.018971,1.03529,-3.77125,1.98625,-5.5475,0.3724,0.112,-0.54236,41.4877,-89.5063,181.347,13,1,33.37,1025.94,-112.301,0.0839844,0.113281,1.05469,1,1,1,1,0,-111.904,0.215423,0.0248211,-111.993,8.03408 +1945715,-0.114314,-0.041297,1.03133,-5.53875,2.1,1.72375,0.38822,0.12292,-0.53424,41.4877,-89.5063,181.347,13,1,33.37,1025.96,-112.477,0.0839844,0.113281,1.05664,1,1,1,1,0,-111.904,0.216057,0.0602339,-111.986,6.88359 +1945725,-0.114314,-0.041297,1.03133,-5.53875,2.1,1.72375,0.38822,0.12292,-0.53424,41.4877,-89.5063,181.347,13,1,33.37,1025.96,-112.477,0.0839844,0.113281,1.05664,1,1,1,1,0,-111.904,0.216057,0.0602339,-111.986,6.88359 +1945735,0.232532,-0.013542,1.05835,-3.52625,6.3525,-13.44,0.40572,0.19194,-0.53368,41.4877,-89.5063,181.335,13,1,33.38,1025.96,-112.48,0.0898438,0.119141,1.05859,1,1,1,1,0,-111.904,0.216057,0.0602339,-111.986,7.89873 +1945745,0.232532,-0.013542,1.05835,-3.52625,6.3525,-13.44,0.40572,0.19194,-0.53368,41.4877,-89.5063,181.335,13,1,33.38,1025.96,-112.48,0.0898438,0.119141,1.05859,1,1,1,1,0,-111.904,0.216057,0.0602339,-111.986,7.89873 +1945755,0.232532,-0.013542,1.05835,-3.52625,6.3525,-13.44,0.40572,0.19194,-0.53368,41.4877,-89.5063,181.335,13,1,33.38,1025.96,-112.48,0.0898438,0.119141,1.05859,1,1,1,1,0,-111.904,0.216057,0.0602339,-111.986,7.89873 +1945765,-0.0244,-0.016043,1.04292,-1.47,-0.5775,-16.6163,0.3731,0.11424,-0.5348,41.4877,-89.5063,181.335,13,1,33.38,1025.96,-112.48,0.0917969,0.115234,1.06055,1,1,1,1,0,-111.9,0.216966,0.0303143,-111.986,8.03408 +1945775,-0.0244,-0.016043,1.04292,-1.47,-0.5775,-16.6163,0.3731,0.11424,-0.5348,41.4877,-89.5063,181.335,13,1,33.38,1025.96,-112.48,0.0917969,0.115234,1.06055,1,1,1,1,0,-111.9,0.216966,0.0303143,-111.986,8.03408 +1945785,-0.0244,-0.016043,1.04292,-1.47,-0.5775,-16.6163,0.3731,0.11424,-0.5348,41.4877,-89.5063,181.335,13,1,33.38,1025.96,-112.48,0.0917969,0.115234,1.06055,1,1,1,1,0,-111.9,0.216966,0.0303143,-111.986,8.03408 +1945795,-0.027389,-0.008723,1.04426,-4.78625,4.165,-5.06625,0.37898,0.1127,-0.54236,41.4877,-89.5063,181.335,13,1,33.38,1025.94,-112.305,0.0839844,0.115234,1.05469,1,1,1,1,0,-111.9,0.216966,0.0303143,-111.986,8.03408 +1945805,-0.027389,-0.008723,1.04426,-4.78625,4.165,-5.06625,0.37898,0.1127,-0.54236,41.4877,-89.5063,181.335,13,1,33.38,1025.94,-112.305,0.0839844,0.115234,1.05469,1,1,1,1,0,-111.9,0.216966,0.0303143,-111.986,8.03408 +1945815,-0.02928,-0.009882,1.0431,-4.9,7.77,-4.94375,0.40278,0.18662,-0.53214,41.4877,-89.5063,181.335,13,1,33.38,1025.98,-112.654,0.0820312,0.117188,1.05273,1,1,1,1,0,-111.901,0.21642,0.0268953,-111.982,8.07275 +1945825,-0.02928,-0.009882,1.0431,-4.9,7.77,-4.94375,0.40278,0.18662,-0.53214,41.4877,-89.5063,181.335,13,1,33.38,1025.98,-112.654,0.0820312,0.117188,1.05273,1,1,1,1,0,-111.901,0.21642,0.0268953,-111.982,8.07275 +1945835,-0.02928,-0.009882,1.0431,-4.9,7.77,-4.94375,0.40278,0.18662,-0.53214,41.4877,-89.5063,181.335,13,1,33.38,1025.98,-112.654,0.0820312,0.117188,1.05273,1,1,1,1,0,-111.901,0.21642,0.0268953,-111.982,8.07275 +1945845,-0.065819,-0.015067,1.03706,-5.38125,4.24375,4.3925,0.37184,0.11508,-0.51254,41.4877,-89.5063,181.335,13,1,33.38,1025.97,-112.567,0.0820312,0.125,1.05469,1,1,1,1,0,-111.901,0.21642,0.0268953,-111.982,8.02441 +1945855,-0.065819,-0.015067,1.03706,-5.38125,4.24375,4.3925,0.37184,0.11508,-0.51254,41.4877,-89.5063,181.335,13,1,33.38,1025.97,-112.567,0.0820312,0.125,1.05469,1,1,1,1,0,-111.901,0.21642,0.0268953,-111.982,8.02441 +1945865,-0.388265,-0.065514,1.03194,-5.11,2.52875,0.55125,0.39256,0.14028,-0.52962,41.4877,-89.5063,181.335,13,1,33.38,1025.97,-112.567,0.0820312,0.125,1.05469,1,1,1,1,0,-111.901,0.21642,0.0268953,-111.982,8.07275 +1945875,-0.388265,-0.065514,1.03194,-5.11,2.52875,0.55125,0.39256,0.14028,-0.52962,41.4877,-89.5063,181.335,13,1,33.38,1025.97,-112.567,0.0859375,0.125,1.05469,1,1,1,1,0,-111.904,0.217295,0.0774332,-111.984,8.07275 +1945885,-0.388265,-0.065514,1.03194,-5.11,2.52875,0.55125,0.39256,0.14028,-0.52962,41.4877,-89.5063,181.335,13,1,33.38,1025.97,-112.567,0.0859375,0.125,1.05469,1,1,1,1,0,-111.904,0.217295,0.0774332,-111.984,8.07275 +1945895,0.21167,-0.010675,1.03633,-4.4275,0.98875,-2.135,0.39606,0.20104,-0.511,41.4877,-89.5063,181.335,13,1,33.39,1025.98,-112.658,0.0859375,0.121094,1.06055,1,1,1,1,0,-111.904,0.217295,0.0774332,-111.984,8.04375 +1945905,0.21167,-0.010675,1.03633,-4.4275,0.98875,-2.135,0.39606,0.20104,-0.511,41.4877,-89.5063,181.335,13,1,33.39,1025.98,-112.658,0.0859375,0.121094,1.06055,1,1,1,1,0,-111.904,0.217295,0.0774332,-111.984,8.04375 +1945915,0.21167,-0.010675,1.03633,-4.4275,0.98875,-2.135,0.39606,0.20104,-0.511,41.4877,-89.5063,181.335,13,1,33.39,1025.98,-112.658,0.0859375,0.121094,1.06055,1,1,1,1,0,-111.904,0.217295,0.0774332,-111.984,8.04375 +1945925,-0.035258,-0.024461,1.0284,-4.15625,2.38875,-3.115,0.39942,0.1022,-0.50246,41.4877,-89.5063,181.335,13,1,33.39,1025.97,-112.57,0.0878906,0.125,1.06445,1,1,1,1,0,-111.903,0.218372,0.031941,-111.986,8.04375 +1945935,-0.035258,-0.024461,1.0284,-4.15625,2.38875,-3.115,0.39942,0.1022,-0.50246,41.4877,-89.5063,181.335,13,1,33.39,1025.97,-112.57,0.0878906,0.125,1.06445,1,1,1,1,0,-111.903,0.218372,0.031941,-111.986,8.04375 +1945945,-0.006283,-0.021777,1.02846,-3.675,-2.275,-2.59,0.39746,0.10696,-0.50582,41.4877,-89.5063,181.345,13,1,33.39,1025.96,-112.484,0.0917969,0.125,1.05469,1,1,1,1,0,-111.903,0.218372,0.031941,-111.986,8.03408 +1945955,-0.006283,-0.021777,1.02846,-3.675,-2.275,-2.59,0.39746,0.10696,-0.50582,41.4877,-89.5063,181.345,13,1,33.39,1025.96,-112.484,0.0917969,0.125,1.05469,1,1,1,1,0,-111.903,0.218372,0.031941,-111.986,8.03408 +1945965,-0.006283,-0.021777,1.02846,-3.675,-2.275,-2.59,0.39746,0.10696,-0.50582,41.4877,-89.5063,181.345,13,1,33.39,1025.96,-112.484,0.0917969,0.125,1.05469,1,1,1,1,0,-111.903,0.218372,0.031941,-111.986,8.03408 +1945975,0.013908,-0.025681,1.04401,-4.8825,1.86375,7.0525,0.39704,0.10094,-0.51954,41.4877,-89.5063,181.345,13,1,33.39,1025.97,-112.57,0.0839844,0.125,1.05469,1,1,1,1,0,-111.905,0.219054,0.0609235,-111.985,8.01475 +1945985,0.013908,-0.025681,1.04401,-4.8825,1.86375,7.0525,0.39704,0.10094,-0.51954,41.4877,-89.5063,181.345,13,1,33.39,1025.97,-112.57,0.0839844,0.125,1.05469,1,1,1,1,0,-111.905,0.219054,0.0609235,-111.985,8.01475 +1945995,0.013908,-0.025681,1.04401,-4.8825,1.86375,7.0525,0.39704,0.10094,-0.51954,41.4877,-89.5063,181.345,13,1,33.39,1025.97,-112.57,0.0839844,0.125,1.05469,1,1,1,1,0,-111.905,0.219054,0.0609235,-111.985,8.01475 +1946005,-0.033672,-0.019581,1.04017,-4.34875,3.64875,-0.0875,0.36386,0.10752,-0.56042,41.4877,-89.5063,181.345,13,1,33.39,1025.97,-112.57,0.078125,0.125,1.05859,1,1,1,1,0,-111.905,0.219054,0.0609235,-111.985,8.04375 +1946015,-0.033672,-0.019581,1.04017,-4.34875,3.64875,-0.0875,0.36386,0.10752,-0.56042,41.4877,-89.5063,181.345,13,1,33.39,1025.97,-112.57,0.078125,0.125,1.05859,1,1,1,1,0,-111.905,0.219054,0.0609235,-111.985,8.04375 +1946025,-0.393755,-0.036295,1.05713,-4.62875,6.58875,-3.47375,0.39214,0.10598,-0.51716,41.4877,-89.5063,181.345,13,1,33.39,1025.95,-112.396,0.0859375,0.125,1.0625,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.03408 +1946035,-0.393755,-0.036295,1.05713,-4.62875,6.58875,-3.47375,0.39214,0.10598,-0.51716,41.4877,-89.5063,181.345,13,1,33.39,1025.95,-112.396,0.0859375,0.125,1.0625,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.03408 +1946045,-0.393755,-0.036295,1.05713,-4.62875,6.58875,-3.47375,0.39214,0.10598,-0.51716,41.4877,-89.5063,181.345,13,1,33.39,1025.95,-112.396,0.0859375,0.125,1.0625,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.03408 +1946055,-0.021899,-0.021899,1.0478,-5.565,5.36375,-0.70875,0.3962,0.17752,-0.56798,41.4877,-89.5063,181.345,13,1,33.39,1025.96,-112.484,0.0898438,0.123047,1.06055,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.02441 +1946065,-0.021899,-0.021899,1.0478,-5.565,5.36375,-0.70875,0.3962,0.17752,-0.56798,41.4877,-89.5063,181.345,13,1,33.39,1025.96,-112.484,0.0898438,0.123047,1.06055,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.02441 +1946075,0.009516,-0.0244,1.03139,-0.16625,-0.65625,-22.2775,0.4088,0.10906,-0.51926,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0898438,0.123047,1.06055,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.06309 +1946085,0.009516,-0.0244,1.03139,-0.16625,-0.65625,-22.2775,0.4088,0.10906,-0.51926,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0898438,0.123047,1.06055,1,1,1,1,0,-111.901,0.222011,0.0817861,-111.986,8.06309 +1946095,0.009516,-0.0244,1.03139,-0.16625,-0.65625,-22.2775,0.4088,0.10906,-0.51926,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0898438,0.125,1.05859,1,1,1,1,0,-111.904,0.221517,0.0160166,-111.981,8.06309 +1946105,0.009516,-0.0244,1.03139,-0.16625,-0.65625,-22.2775,0.4088,0.10906,-0.51926,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0898438,0.125,1.05859,1,1,1,1,0,-111.904,0.221517,0.0160166,-111.981,8.06309 +1946115,0.034831,-0.01891,1.03779,-1.2775,0.74375,-24.8237,0.40964,0.19096,-0.53564,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0820312,0.109375,1.05273,1,1,1,1,0,-111.904,0.221517,0.0160166,-111.981,8.07275 +1946125,0.034831,-0.01891,1.03779,-1.2775,0.74375,-24.8237,0.40964,0.19096,-0.53564,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0820312,0.109375,1.05273,1,1,1,1,0,-111.904,0.221517,0.0160166,-111.981,8.07275 +1946135,0.034831,-0.01891,1.03779,-1.2775,0.74375,-24.8237,0.40964,0.19096,-0.53564,41.4877,-89.5063,181.345,13,1,33.37,1025.98,-112.651,0.0820312,0.109375,1.05273,1,1,1,1,0,-111.904,0.221517,0.0160166,-111.981,8.07275 +1946145,0.029158,-0.005124,1.03706,-4.64625,3.38625,-0.13125,0.40516,0.18494,-0.53214,41.4877,-89.5063,181.351,13,1,33.37,1025.96,-112.477,0.0820312,0.107422,1.05469,1,1,1,1,0,-111.905,0.220548,0.0252303,-111.984,8.05342 +1946155,0.029158,-0.005124,1.03706,-4.64625,3.38625,-0.13125,0.40516,0.18494,-0.53214,41.4877,-89.5063,181.351,13,1,33.37,1025.96,-112.477,0.0820312,0.107422,1.05469,1,1,1,1,0,-111.905,0.220548,0.0252303,-111.984,8.05342 +1946165,0.004148,-0.015677,1.03389,-2.905,1.645,-11.7425,0.37828,0.1071,-0.54516,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0839844,0.105469,1.05469,1,1,1,1,0,-111.905,0.220548,0.0252303,-111.984,8.06309 +1946175,0.004148,-0.015677,1.03389,-2.905,1.645,-11.7425,0.37828,0.1071,-0.54516,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0839844,0.105469,1.05469,1,1,1,1,0,-111.905,0.220548,0.0252303,-111.984,8.06309 +1946185,0.004148,-0.015677,1.03389,-2.905,1.645,-11.7425,0.37828,0.1071,-0.54516,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0839844,0.105469,1.05469,1,1,1,1,0,-111.905,0.220548,0.0252303,-111.984,8.06309 +1946195,0.019703,-0.0122,1.03194,-3.92875,2.70375,-3.6575,0.40754,0.17766,-0.52906,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0859375,0.113281,1.05273,1,1,1,1,0,-111.904,0.220711,0.0432612,-111.986,8.06309 +1946205,0.019703,-0.0122,1.03194,-3.92875,2.70375,-3.6575,0.40754,0.17766,-0.52906,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0859375,0.113281,1.05273,1,1,1,1,0,-111.904,0.220711,0.0432612,-111.986,8.06309 +1946215,0.019703,-0.0122,1.03194,-3.92875,2.70375,-3.6575,0.40754,0.17766,-0.52906,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0859375,0.113281,1.05273,1,1,1,1,0,-111.904,0.220711,0.0432612,-111.986,8.06309 +1946225,-0.006222,-0.010492,1.03548,-3.6575,2.1,-5.0925,0.4074,0.18158,-0.5243,41.4877,-89.5063,181.351,13,1,33.38,1025.96,-112.48,0.0839844,0.113281,1.05664,1,1,1,1,0,-111.904,0.220711,0.0432612,-111.986,7.01895 +1946235,-0.006222,-0.010492,1.03548,-3.6575,2.1,-5.0925,0.4074,0.18158,-0.5243,41.4877,-89.5063,181.351,13,1,33.38,1025.96,-112.48,0.0839844,0.113281,1.05664,1,1,1,1,0,-111.904,0.220711,0.0432612,-111.986,7.01895 +1946245,0.030622,-0.017873,1.03285,-3.7975,2.1,-6.02875,0.37142,0.10794,-0.52598,41.4877,-89.5063,181.351,13,1,33.38,1025.98,-112.654,0.0859375,0.109375,1.05664,1,1,1,1,0,-111.905,0.219446,-0.00558164,-111.985,7.50234 +1946255,0.030622,-0.017873,1.03285,-3.7975,2.1,-6.02875,0.37142,0.10794,-0.52598,41.4877,-89.5063,181.351,13,1,33.38,1025.98,-112.654,0.0859375,0.109375,1.05664,1,1,1,1,0,-111.905,0.219446,-0.00558164,-111.985,7.50234 +1946265,0.030622,-0.017873,1.03285,-3.7975,2.1,-6.02875,0.37142,0.10794,-0.52598,41.4877,-89.5063,181.351,13,1,33.38,1025.98,-112.654,0.0859375,0.109375,1.05664,1,1,1,1,0,-111.905,0.219446,-0.00558164,-111.985,7.50234 +1946275,-0.007442,-0.010309,1.037,-4.41875,3.28125,1.40875,0.378,0.11746,-0.52388,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0898438,0.0996094,1.05078,1,1,1,1,0,-111.905,0.219446,-0.00558164,-111.985,8.00508 +1946285,-0.007442,-0.010309,1.037,-4.41875,3.28125,1.40875,0.378,0.11746,-0.52388,41.4877,-89.5063,181.351,13,1,33.38,1025.97,-112.567,0.0898438,0.0996094,1.05078,1,1,1,1,0,-111.905,0.219446,-0.00558164,-111.985,8.00508 +1946295,-0.008052,-0.008723,1.04048,-2.31875,2.24875,-17.4125,0.39382,0.16002,-0.52248,41.4877,-89.5063,181.351,13,1,33.38,1025.96,-112.48,0.0898438,0.0996094,1.05078,1,1,1,1,0,-111.905,0.219446,-0.00558164,-111.985,8.00508 +1946305,-0.008052,-0.008723,1.04048,-2.31875,2.24875,-17.4125,0.39382,0.16002,-0.52248,41.4877,-89.5063,181.351,13,1,33.38,1025.96,-112.48,0.09375,0.0996094,1.04688,1,1,1,1,0,-111.901,0.217247,-0.0281157,-111.987,8.01475 +1946315,-0.008052,-0.008723,1.04048,-2.31875,2.24875,-17.4125,0.39382,0.16002,-0.52248,41.4877,-89.5063,181.351,13,1,33.38,1025.96,-112.48,0.09375,0.0996094,1.04688,1,1,1,1,0,-111.901,0.217247,-0.0281157,-111.987,8.01475 +1946325,-0.00183,-0.007747,1.02913,-4.8825,-0.35,3.00125,0.3934,0.10066,-0.49602,41.4877,-89.5063,181.351,13,1,33.39,1025.94,-112.309,0.0878906,0.107422,1.04883,1,1,1,1,0,-111.901,0.217247,-0.0281157,-111.987,8.05342 +1946335,-0.00183,-0.007747,1.02913,-4.8825,-0.35,3.00125,0.3934,0.10066,-0.49602,41.4877,-89.5063,181.351,13,1,33.39,1025.94,-112.309,0.0878906,0.107422,1.04883,1,1,1,1,0,-111.901,0.217247,-0.0281157,-111.987,8.05342 +1946345,-0.00183,-0.007747,1.02913,-4.8825,-0.35,3.00125,0.3934,0.10066,-0.49602,41.4877,-89.5063,181.351,13,1,33.39,1025.94,-112.309,0.0878906,0.107422,1.04883,1,1,1,1,0,-111.901,0.217247,-0.0281157,-111.987,8.05342 +1946355,0.010004,-0.015494,1.04005,-2.00375,-1.47,-10.4913,0.36358,0.10542,-0.56378,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0839844,0.0996094,1.05469,1,1,1,1,0,-111.904,0.214772,0.00319703,-111.983,8.01475 +1946365,0.010004,-0.015494,1.04005,-2.00375,-1.47,-10.4913,0.36358,0.10542,-0.56378,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0839844,0.0996094,1.05469,1,1,1,1,0,-111.904,0.214772,0.00319703,-111.983,8.01475 +1946375,0.004636,-0.014823,1.04365,-7.25375,2.87875,5.3375,0.3976,0.15064,-0.56196,41.4877,-89.5063,181.343,13,1,33.39,1025.98,-112.658,0.0820312,0.0917969,1.05273,1,1,1,1,0,-111.904,0.214772,0.00319703,-111.983,8.05342 +1946385,0.004636,-0.014823,1.04365,-7.25375,2.87875,5.3375,0.3976,0.15064,-0.56196,41.4877,-89.5063,181.343,13,1,33.39,1025.98,-112.658,0.0820312,0.0917969,1.05273,1,1,1,1,0,-111.904,0.214772,0.00319703,-111.983,8.05342 +1946395,0.004636,-0.014823,1.04365,-7.25375,2.87875,5.3375,0.3976,0.15064,-0.56196,41.4877,-89.5063,181.343,13,1,33.39,1025.98,-112.658,0.0820312,0.0917969,1.05273,1,1,1,1,0,-111.904,0.214772,0.00319703,-111.983,8.05342 +1946405,0.003172,-0.022814,1.04005,-3.40375,5.78375,-7.245,0.39466,0.09674,-0.5145,41.4877,-89.5063,181.343,13,1,33.39,1025.96,-112.484,0.0898438,0.0898438,1.05078,1,1,1,1,0,-111.905,0.21332,0.0068286,-111.987,8.01475 +1946415,0.003172,-0.022814,1.04005,-3.40375,5.78375,-7.245,0.39466,0.09674,-0.5145,41.4877,-89.5063,181.343,13,1,33.39,1025.96,-112.484,0.0898438,0.0898438,1.05078,1,1,1,1,0,-111.905,0.21332,0.0068286,-111.987,8.01475 +1946425,-0.004514,-0.015006,1.04505,-2.47625,1.68,-9.275,0.39312,0.1043,-0.51548,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0898438,0.0898438,1.05078,1,1,1,1,0,-111.905,0.21332,0.0068286,-111.987,8.01475 +1946435,-0.004514,-0.015006,1.04505,-2.47625,1.68,-9.275,0.39312,0.1043,-0.51548,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0917969,0.09375,1.05273,1,1,1,1,0,-111.905,0.21332,0.0068286,-111.987,8.05342 +1946445,-0.004514,-0.015006,1.04505,-2.47625,1.68,-9.275,0.39312,0.1043,-0.51548,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0917969,0.09375,1.05273,1,1,1,1,0,-111.905,0.21332,0.0068286,-111.987,8.05342 +1946455,0.010187,-0.014701,1.0367,-3.28125,1.3825,-6.7025,0.39704,0.10038,-0.50904,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0859375,0.0996094,1.05469,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.04375 +1946465,0.010187,-0.014701,1.0367,-3.28125,1.3825,-6.7025,0.39704,0.10038,-0.50904,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0859375,0.0996094,1.05469,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.04375 +1946475,0.010187,-0.014701,1.0367,-3.28125,1.3825,-6.7025,0.39704,0.10038,-0.50904,41.4877,-89.5063,181.343,13,1,33.39,1025.97,-112.57,0.0859375,0.0996094,1.05469,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.04375 +1946485,-0.011102,-0.025803,1.02754,-3.71875,1.84625,-5.5825,0.40292,0.10836,-0.51212,41.4877,-89.5063,181.343,13,1,33.37,1025.96,-112.477,0.0859375,0.0996094,1.05469,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.05342 +1946495,-0.011102,-0.025803,1.02754,-3.71875,1.84625,-5.5825,0.40292,0.10836,-0.51212,41.4877,-89.5063,181.343,13,1,33.37,1025.96,-112.477,0.0859375,0.0996094,1.05469,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.05342 +1946505,-0.011102,-0.025803,1.02754,-3.71875,1.84625,-5.5825,0.40292,0.10836,-0.51212,41.4877,-89.5063,181.343,13,1,33.37,1025.96,-112.477,0.0859375,0.101562,1.05664,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.05342 +1946515,-0.011102,-0.025803,1.02754,-3.71875,1.84625,-5.5825,0.40292,0.10836,-0.51212,41.4877,-89.5063,181.343,13,1,33.37,1025.96,-112.477,0.0859375,0.101562,1.05664,1,1,1,1,0,-111.902,0.212697,0.0245548,-111.988,8.05342 +1946525,-0.391498,-0.041236,1.05786,-3.605,1.8725,-5.43375,0.40712,0.18648,-0.5334,41.4877,-89.5063,181.343,13,1,33.37,1025.96,-112.477,0.0859375,0.0976562,1.05469,1,1,1,1,0,-111.905,0.212418,0.0432153,-111.985,8.06309 +1946535,-0.391498,-0.041236,1.05786,-3.605,1.8725,-5.43375,0.40712,0.18648,-0.5334,41.4877,-89.5063,181.343,13,1,33.37,1025.96,-112.477,0.0859375,0.0976562,1.05469,1,1,1,1,0,-111.905,0.212418,0.0432153,-111.985,8.06309 +1946545,0.016348,-0.036661,1.01949,-5.15375,2.87875,6.055,0.406,0.18186,-0.5292,41.4877,-89.5063,181.308,13,1,33.38,1025.98,-112.654,0.0898438,0.0976562,1.05664,1,1,1,1,0,-111.905,0.212418,0.0432153,-111.985,8.05342 +1946555,0.016348,-0.036661,1.01949,-5.15375,2.87875,6.055,0.406,0.18186,-0.5292,41.4877,-89.5063,181.308,13,1,33.38,1025.98,-112.654,0.0898438,0.0976562,1.05664,1,1,1,1,0,-111.905,0.212418,0.0432153,-111.985,8.05342 +1946565,0.016348,-0.036661,1.01949,-5.15375,2.87875,6.055,0.406,0.18186,-0.5292,41.4877,-89.5063,181.308,13,1,33.38,1025.98,-112.654,0.0898438,0.0976562,1.05664,1,1,1,1,0,-111.905,0.212418,0.0432153,-111.985,8.05342 +1946575,-0.003111,-0.030927,1.03804,-4.13,0.49,-3.325,0.37744,0.1092,-0.5313,41.4877,-89.5063,181.308,13,1,33.38,1025.96,-112.48,0.09375,0.0976562,1.05469,1,1,1,1,0,-111.901,0.211355,-0.0225558,-111.988,8.04375 +1946585,-0.003111,-0.030927,1.03804,-4.13,0.49,-3.325,0.37744,0.1092,-0.5313,41.4877,-89.5063,181.308,13,1,33.38,1025.96,-112.48,0.09375,0.0976562,1.05469,1,1,1,1,0,-111.901,0.211355,-0.0225558,-111.988,8.04375 +1946595,0.011285,-0.032208,1.02376,-6.60625,4.73375,9.51125,0.39732,0.15582,-0.52668,41.4877,-89.5063,181.308,13,1,33.38,1025.94,-112.305,0.09375,0.0976562,1.05469,1,1,1,1,0,-111.901,0.211355,-0.0225558,-111.988,8.05342 +1946605,0.011285,-0.032208,1.02376,-6.60625,4.73375,9.51125,0.39732,0.15582,-0.52668,41.4877,-89.5063,181.308,13,1,33.38,1025.94,-112.305,0.0917969,0.103516,1.04883,1,1,1,1,0,-111.901,0.211355,-0.0225558,-111.988,8.05342 +1946615,0.011285,-0.032208,1.02376,-6.60625,4.73375,9.51125,0.39732,0.15582,-0.52668,41.4877,-89.5063,181.308,13,1,33.38,1025.94,-112.305,0.0917969,0.103516,1.04883,1,1,1,1,0,-111.901,0.211355,-0.0225558,-111.988,8.05342 +1946625,-0.027938,-0.031964,1.02578,-3.2375,6.34375,-11.2262,0.40474,0.18004,-0.52234,41.4877,-89.5063,181.308,13,1,33.38,1025.94,-112.305,0.09375,0.105469,1.05078,1,1,1,1,0,-111.899,0.210238,0.0208775,-111.985,8.05342 +1946635,-0.027938,-0.031964,1.02578,-3.2375,6.34375,-11.2262,0.40474,0.18004,-0.52234,41.4877,-89.5063,181.308,13,1,33.38,1025.94,-112.305,0.09375,0.105469,1.05078,1,1,1,1,0,-111.899,0.210238,0.0208775,-111.985,8.05342 +1946645,-0.027938,-0.031964,1.02578,-3.2375,6.34375,-11.2262,0.40474,0.18004,-0.52234,41.4877,-89.5063,181.308,13,1,33.38,1025.94,-112.305,0.09375,0.105469,1.05078,1,1,1,1,0,-111.899,0.210238,0.0208775,-111.985,8.05342 +1946655,0.016592,-0.025376,1.03291,-4.9,3.68375,4.19125,0.37548,0.11522,-0.53298,41.4877,-89.5063,181.308,13,1,33.38,1025.95,-112.393,0.0957031,0.105469,1.05469,1,1,1,1,0,-111.899,0.210238,0.0208775,-111.985,8.05342 +1946665,0.016592,-0.025376,1.03291,-4.9,3.68375,4.19125,0.37548,0.11522,-0.53298,41.4877,-89.5063,181.308,13,1,33.38,1025.95,-112.393,0.0957031,0.105469,1.05469,1,1,1,1,0,-111.899,0.210238,0.0208775,-111.985,8.05342 +1946675,-0.238205,-0.047458,1.03261,-5.31125,3.1675,5.4775,0.4123,0.19208,-0.5348,41.4877,-89.5063,181.308,13,1,33.38,1025.96,-112.48,0.0839844,0.109375,1.05469,1,1,1,1,0,-111.898,0.211635,0.0767539,-111.983,8.07275 +1946685,-0.238205,-0.047458,1.03261,-5.31125,3.1675,5.4775,0.4123,0.19208,-0.5348,41.4877,-89.5063,181.308,13,1,33.38,1025.96,-112.48,0.0839844,0.109375,1.05469,1,1,1,1,0,-111.898,0.211635,0.0767539,-111.983,8.07275 +1946695,-0.238205,-0.047458,1.03261,-5.31125,3.1675,5.4775,0.4123,0.19208,-0.5348,41.4877,-89.5063,181.308,13,1,33.38,1025.96,-112.48,0.0839844,0.109375,1.05469,1,1,1,1,0,-111.898,0.211635,0.0767539,-111.983,8.07275 +1946705,0.006466,-0.023973,1.04359,-4.29625,3.49125,-5.78375,0.40012,0.10262,-0.50946,41.4877,-89.5063,181.308,13,1,33.39,1025.96,-112.484,0.0820312,0.111328,1.06055,1,1,1,1,0,-111.898,0.211635,0.0767539,-111.983,7.99541 +1946715,0.006466,-0.023973,1.04359,-4.29625,3.49125,-5.78375,0.40012,0.10262,-0.50946,41.4877,-89.5063,181.308,13,1,33.39,1025.96,-112.484,0.0820312,0.111328,1.06055,1,1,1,1,0,-111.898,0.211635,0.0767539,-111.983,7.99541 +1946725,-0.015982,-0.026291,1.0212,-4.78625,0.98875,-0.49,0.3969,0.10066,-0.51198,41.4877,-89.5063,181.308,13,1,33.38,1025.95,-112.393,0.0820312,0.111328,1.06055,1,1,1,1,0,-111.898,0.211635,0.0767539,-111.983,7.99541 +1946735,-0.015982,-0.026291,1.0212,-4.78625,0.98875,-0.49,0.3969,0.10066,-0.51198,41.4877,-89.5063,181.308,13,1,33.38,1025.95,-112.393,0.0800781,0.113281,1.05273,1,1,1,1,0,-111.898,0.212696,0.03219,-111.982,8.03408 +1946745,-0.015982,-0.026291,1.0212,-4.78625,0.98875,-0.49,0.3969,0.10066,-0.51198,41.4877,-89.5063,181.308,13,1,33.38,1025.95,-112.393,0.0800781,0.113281,1.05273,1,1,1,1,0,-111.898,0.212696,0.03219,-111.982,8.03408 +1946755,-0.018727,-0.025925,1.04176,-3.57875,2.1525,-6.335,0.3941,0.17584,-0.56196,41.4877,-89.5063,181.316,13,1,33.39,1025.96,-112.484,0.0800781,0.119141,1.05469,1,1,1,1,0,-111.898,0.212696,0.03219,-111.982,8.04375 +1946765,-0.018727,-0.025925,1.04176,-3.57875,2.1525,-6.335,0.3941,0.17584,-0.56196,41.4877,-89.5063,181.316,13,1,33.39,1025.96,-112.484,0.0800781,0.119141,1.05469,1,1,1,1,0,-111.898,0.212696,0.03219,-111.982,8.04375 +1946775,-0.018727,-0.025925,1.04176,-3.57875,2.1525,-6.335,0.3941,0.17584,-0.56196,41.4877,-89.5063,181.316,13,1,33.39,1025.96,-112.484,0.0800781,0.119141,1.05469,1,1,1,1,0,-111.898,0.212696,0.03219,-111.982,8.04375 +1946785,-0.052582,-0.010797,1.02492,-3.99875,2.03,-5.83625,0.3969,0.18382,-0.56406,41.4877,-89.5063,181.316,13,1,33.39,1025.97,-112.57,0.0839844,0.119141,1.05469,1,1,1,1,0,-111.894,0.214887,0.0780495,-111.981,8.04375 +1946795,-0.052582,-0.010797,1.02492,-3.99875,2.03,-5.83625,0.3969,0.18382,-0.56406,41.4877,-89.5063,181.316,13,1,33.39,1025.97,-112.57,0.0839844,0.119141,1.05469,1,1,1,1,0,-111.894,0.214887,0.0780495,-111.981,8.04375 +1946805,0.134627,-0.006466,1.06036,-3.8675,2.345,-4.15625,0.39508,0.10556,-0.50456,41.4877,-89.5063,181.316,13,1,33.39,1025.94,-112.309,0.0878906,0.111328,1.06055,1,1,1,1,0,-111.894,0.214887,0.0780495,-111.981,8.01475 +1946815,0.134627,-0.006466,1.06036,-3.8675,2.345,-4.15625,0.39508,0.10556,-0.50456,41.4877,-89.5063,181.316,13,1,33.39,1025.94,-112.309,0.0878906,0.111328,1.06055,1,1,1,1,0,-111.894,0.214887,0.0780495,-111.981,8.01475 +1946825,0.134627,-0.006466,1.06036,-3.8675,2.345,-4.15625,0.39508,0.10556,-0.50456,41.4877,-89.5063,181.316,13,1,33.39,1025.94,-112.309,0.0878906,0.111328,1.06055,1,1,1,1,0,-111.894,0.214887,0.0780495,-111.981,8.01475 +1946835,0.012017,0.000854,1.04639,-3.465,3.8675,-5.90625,0.3745,0.11508,-0.51366,41.4877,-89.5063,181.316,13,1,33.39,1025.97,-112.57,0.0878906,0.115234,1.06055,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.01475 +1946845,0.012017,0.000854,1.04639,-3.465,3.8675,-5.90625,0.3745,0.11508,-0.51366,41.4877,-89.5063,181.316,13,1,33.39,1025.97,-112.57,0.0878906,0.115234,1.06055,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.01475 +1946855,0.012017,0.000854,1.04639,-3.465,3.8675,-5.90625,0.3745,0.11508,-0.51366,41.4877,-89.5063,181.316,13,1,33.39,1025.97,-112.57,0.0878906,0.115234,1.06055,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.01475 +1946865,-0.039467,-0.009943,1.02693,-3.5,-3.70125,-1.32125,0.39354,0.1799,-0.5152,41.4877,-89.5063,181.316,13,1,33.39,1025.96,-112.484,0.0820312,0.125,1.05078,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.05342 +1946875,-0.039467,-0.009943,1.02693,-3.5,-3.70125,-1.32125,0.39354,0.1799,-0.5152,41.4877,-89.5063,181.316,13,1,33.39,1025.96,-112.484,0.0820312,0.125,1.05078,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.05342 +1946885,-0.035319,-0.01403,1.03401,-5.53,5.845,-4.89125,0.37716,0.10836,-0.581,41.4877,-89.5063,181.316,13,1,33.37,1025.96,-112.477,0.0820312,0.125,1.05078,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.07275 +1946895,-0.035319,-0.01403,1.03401,-5.53,5.845,-4.89125,0.37716,0.10836,-0.581,41.4877,-89.5063,181.316,13,1,33.37,1025.96,-112.477,0.0820312,0.125,1.05078,1,1,1,1,0,-111.889,0.215023,-0.00378238,-111.977,8.07275 +1946905,-0.035319,-0.01403,1.03401,-5.53,5.845,-4.89125,0.37716,0.10836,-0.581,41.4877,-89.5063,181.316,13,1,33.37,1025.96,-112.477,0.0800781,0.123047,1.05078,1,1,1,1,0,-111.887,0.212973,-0.0109135,-111.972,8.07275 +1946915,-0.035319,-0.01403,1.03401,-5.53,5.845,-4.89125,0.37716,0.10836,-0.581,41.4877,-89.5063,181.316,13,1,33.37,1025.96,-112.477,0.0800781,0.123047,1.05078,1,1,1,1,0,-111.887,0.212973,-0.0109135,-111.972,8.07275 +1946925,-0.071614,-0.013725,1.03419,-6.4925,2.415,10.7975,0.4081,0.17038,-0.53256,41.4877,-89.5063,181.316,13,1,33.37,1025.95,-112.389,0.0859375,0.125,1.05078,1,1,1,1,0,-111.887,0.212973,-0.0109135,-111.972,8.04375 +1946935,-0.071614,-0.013725,1.03419,-6.4925,2.415,10.7975,0.4081,0.17038,-0.53256,41.4877,-89.5063,181.316,13,1,33.37,1025.95,-112.389,0.0859375,0.125,1.05078,1,1,1,1,0,-111.887,0.212973,-0.0109135,-111.972,8.04375 +1946945,-0.071614,-0.013725,1.03419,-6.4925,2.415,10.7975,0.4081,0.17038,-0.53256,41.4877,-89.5063,181.316,13,1,33.37,1025.95,-112.389,0.0859375,0.125,1.05078,1,1,1,1,0,-111.887,0.212973,-0.0109135,-111.972,8.04375 +1946955,-0.032574,-0.020862,1.03139,-4.24375,-0.56,-2.65125,0.38976,0.10822,-0.53214,41.4877,-89.5063,181.301,13,1,33.37,1025.95,-112.389,0.0878906,0.126953,1.05469,1,1,1,1,0,-111.888,0.210144,-0.0117631,-111.971,8.06309 +1946965,-0.032574,-0.020862,1.03139,-4.24375,-0.56,-2.65125,0.38976,0.10822,-0.53214,41.4877,-89.5063,181.301,13,1,33.37,1025.95,-112.389,0.0878906,0.126953,1.05469,1,1,1,1,0,-111.888,0.210144,-0.0117631,-111.971,8.06309 +1946975,-0.024705,-0.017324,1.03157,-3.01,1.39125,-15.1112,0.3724,0.11312,-0.53998,41.4877,-89.5063,181.301,13,1,33.38,1025.97,-112.567,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.888,0.210144,-0.0117631,-111.971,8.07275 +1946985,-0.024705,-0.017324,1.03157,-3.01,1.39125,-15.1112,0.3724,0.11312,-0.53998,41.4877,-89.5063,181.301,13,1,33.38,1025.97,-112.567,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.888,0.210144,-0.0117631,-111.971,8.07275 +1946995,-0.024705,-0.017324,1.03157,-3.01,1.39125,-15.1112,0.3724,0.11312,-0.53998,41.4877,-89.5063,181.301,13,1,33.38,1025.97,-112.567,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.888,0.210144,-0.0117631,-111.971,8.07275 +1947005,-0.04026,-0.027267,1.0348,-3.9375,1.8725,-6.67625,0.4151,0.18354,-0.53214,41.4877,-89.5063,181.301,13,1,33.38,1025.94,-112.305,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.888,0.210144,-0.0117631,-111.971,8.07275 +1947015,-0.04026,-0.027267,1.0348,-3.9375,1.8725,-6.67625,0.4151,0.18354,-0.53214,41.4877,-89.5063,181.301,13,1,33.38,1025.94,-112.305,0.0859375,0.119141,1.05078,1,1,1,1,0,-111.888,0.207168,-0.0287611,-111.972,8.03408 +1947025,-0.04026,-0.027267,1.0348,-3.9375,1.8725,-6.67625,0.4151,0.18354,-0.53214,41.4877,-89.5063,181.301,13,1,33.38,1025.94,-112.305,0.0859375,0.119141,1.05078,1,1,1,1,0,-111.888,0.207168,-0.0287611,-111.972,8.03408 +1947035,-0.01586,-0.020679,1.03413,-3.64875,1.70625,-6.81625,0.40684,0.16226,-0.55818,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0839844,0.125,1.04883,1,1,1,1,0,-111.888,0.207168,-0.0287611,-111.972,8.04375 +1947045,-0.01586,-0.020679,1.03413,-3.64875,1.70625,-6.81625,0.40684,0.16226,-0.55818,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0839844,0.125,1.04883,1,1,1,1,0,-111.888,0.207168,-0.0287611,-111.972,8.04375 +1947055,-0.001586,-0.035685,1.02761,-3.82375,1.70625,-6.51875,0.37884,0.10654,-0.53746,41.4877,-89.5063,181.301,13,1,33.38,1025.95,-112.393,0.0859375,0.121094,1.05078,1,1,1,1,0,-111.888,0.207168,-0.0287611,-111.972,8.06309 +1947065,-0.001586,-0.035685,1.02761,-3.82375,1.70625,-6.51875,0.37884,0.10654,-0.53746,41.4877,-89.5063,181.301,13,1,33.38,1025.95,-112.393,0.0859375,0.121094,1.05078,1,1,1,1,0,-111.888,0.204994,0.00306366,-111.973,8.06309 +1947075,-0.001586,-0.035685,1.02761,-3.82375,1.70625,-6.51875,0.37884,0.10654,-0.53746,41.4877,-89.5063,181.301,13,1,33.38,1025.95,-112.393,0.0859375,0.121094,1.05078,1,1,1,1,0,-111.888,0.204994,0.00306366,-111.973,8.06309 +1947085,-0.002013,-0.030134,1.03303,-4.4975,2.94875,-0.7,0.4109,0.1876,-0.52976,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0859375,0.121094,1.05273,1,1,1,1,0,-111.888,0.204994,0.00306366,-111.973,8.01475 +1947095,-0.002013,-0.030134,1.03303,-4.4975,2.94875,-0.7,0.4109,0.1876,-0.52976,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0859375,0.121094,1.05273,1,1,1,1,0,-111.888,0.204994,0.00306366,-111.973,8.01475 +1947105,-0.02684,-0.018666,1.04444,-1.39125,0.455,-17.7625,0.4039,0.10164,-0.50022,41.4877,-89.5063,181.301,13,1,33.38,1025.94,-112.305,0.0839844,0.117188,1.05273,1,1,1,1,0,-111.888,0.204994,0.00306366,-111.973,8.03408 +1947115,-0.02684,-0.018666,1.04444,-1.39125,0.455,-17.7625,0.4039,0.10164,-0.50022,41.4877,-89.5063,181.301,13,1,33.38,1025.94,-112.305,0.0839844,0.117188,1.05273,1,1,1,1,0,-111.888,0.20361,0.00680939,-111.974,8.03408 +1947125,-0.02684,-0.018666,1.04444,-1.39125,0.455,-17.7625,0.4039,0.10164,-0.50022,41.4877,-89.5063,181.301,13,1,33.38,1025.94,-112.305,0.0839844,0.117188,1.05273,1,1,1,1,0,-111.888,0.20361,0.00680939,-111.974,8.03408 +1947135,-0.009943,-0.028487,1.03346,-5.7575,2.59,8.91625,0.40068,0.10248,-0.51324,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0878906,0.115234,1.05273,1,1,1,1,0,-111.888,0.20361,0.00680939,-111.974,7.87939 +1947145,-0.009943,-0.028487,1.03346,-5.7575,2.59,8.91625,0.40068,0.10248,-0.51324,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0878906,0.115234,1.05273,1,1,1,1,0,-111.888,0.20361,0.00680939,-111.974,7.87939 +1947155,-0.009943,-0.028487,1.03346,-5.7575,2.59,8.91625,0.40068,0.10248,-0.51324,41.4877,-89.5063,181.301,13,1,33.38,1025.96,-112.48,0.0878906,0.115234,1.05273,1,1,1,1,0,-111.888,0.20361,0.00680939,-111.974,7.87939 +1947165,0.002074,-0.024339,1.03541,-0.6125,1.47,-18.375,0.39872,0.10178,-0.51086,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0878906,0.115234,1.05078,1,1,1,1,0,-111.89,0.201028,-0.0266479,-111.974,7.98574 +1947175,0.002074,-0.024339,1.03541,-0.6125,1.47,-18.375,0.39872,0.10178,-0.51086,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0878906,0.115234,1.05078,1,1,1,1,0,-111.89,0.201028,-0.0266479,-111.974,7.98574 +1947185,-0.004026,-0.008845,1.02425,-0.4025,-0.735,-24.1325,0.39788,0.16478,-0.5236,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0859375,0.111328,1.04883,1,1,1,1,0,-111.89,0.201028,-0.0266479,-111.974,8.04375 +1947195,-0.004026,-0.008845,1.02425,-0.4025,-0.735,-24.1325,0.39788,0.16478,-0.5236,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0859375,0.111328,1.04883,1,1,1,1,0,-111.89,0.201028,-0.0266479,-111.974,8.04375 +1947205,-0.004026,-0.008845,1.02425,-0.4025,-0.735,-24.1325,0.39788,0.16478,-0.5236,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0859375,0.111328,1.04883,1,1,1,1,0,-111.89,0.201028,-0.0266479,-111.974,8.04375 +1947215,-0.015738,-0.034038,1.03322,-3.64875,5.97625,-8.81125,0.37226,0.1015,-0.5201,41.4877,-89.5063,181.285,13,1,33.39,1025.96,-112.484,0.0839844,0.103516,1.05078,1,1,1,1,0,-111.891,0.198838,0.00342671,-111.977,7.99541 +1947225,-0.015738,-0.034038,1.03322,-3.64875,5.97625,-8.81125,0.37226,0.1015,-0.5201,41.4877,-89.5063,181.285,13,1,33.39,1025.96,-112.484,0.0839844,0.103516,1.05078,1,1,1,1,0,-111.891,0.198838,0.00342671,-111.977,7.99541 +1947235,-0.000244,-0.022814,1.03578,-4.19125,-0.21,0.32375,0.39662,0.1624,-0.56406,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0839844,0.103516,1.05078,1,1,1,1,0,-111.891,0.198838,0.00342671,-111.977,7.99541 +1947245,-0.000244,-0.022814,1.03578,-4.19125,-0.21,0.32375,0.39662,0.1624,-0.56406,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0859375,0.103516,1.05273,1,1,1,1,0,-111.891,0.198838,0.00342671,-111.977,8.04375 +1947255,-0.000244,-0.022814,1.03578,-4.19125,-0.21,0.32375,0.39662,0.1624,-0.56406,41.4877,-89.5063,181.285,13,1,33.39,1025.97,-112.57,0.0859375,0.103516,1.05273,1,1,1,1,0,-111.891,0.198838,0.00342671,-111.977,8.04375 +1947265,-0.018666,-0.021777,1.02944,-3.7975,2.485,-5.9675,0.399,0.1015,-0.51212,41.4877,-89.5063,181.285,13,1,33.39,1025.96,-112.484,0.0898438,0.0996094,1.05273,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.05342 +1947275,-0.018666,-0.021777,1.02944,-3.7975,2.485,-5.9675,0.399,0.1015,-0.51212,41.4877,-89.5063,181.285,13,1,33.39,1025.96,-112.484,0.0898438,0.0996094,1.05273,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.05342 +1947285,-0.018666,-0.021777,1.02944,-3.7975,2.485,-5.9675,0.399,0.1015,-0.51212,41.4877,-89.5063,181.285,13,1,33.39,1025.96,-112.484,0.0898438,0.0996094,1.05273,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.05342 +1947295,-0.07381,-0.029829,1.02663,-3.535,1.645,-5.73125,0.37744,0.11214,-0.52654,41.4877,-89.5063,181.285,13,1,33.37,1025.94,-112.301,0.0898438,0.0996094,1.05273,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.08242 +1947305,-0.07381,-0.029829,1.02663,-3.535,1.645,-5.73125,0.37744,0.11214,-0.52654,41.4877,-89.5063,181.285,13,1,33.37,1025.94,-112.301,0.0898438,0.0996094,1.05273,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.08242 +1947315,-0.07381,-0.029829,1.02663,-3.535,1.645,-5.73125,0.37744,0.11214,-0.52654,41.4877,-89.5063,181.285,13,1,33.37,1025.94,-112.301,0.0898438,0.0996094,1.05078,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.08242 +1947325,-0.07381,-0.029829,1.02663,-3.535,1.645,-5.73125,0.37744,0.11214,-0.52654,41.4877,-89.5063,181.285,13,1,33.37,1025.94,-112.301,0.0898438,0.0996094,1.05078,1,1,1,1,0,-111.886,0.197927,0.00692699,-111.979,8.08242 +1947335,0.309636,0.012444,1.04261,-3.75375,1.61875,-5.495,0.40866,0.19012,-0.52668,41.4877,-89.5063,181.285,13,1,33.37,1025.98,-112.651,0.09375,0.101562,1.05664,1,1,1,1,0,-111.889,0.19777,0.0581912,-111.974,8.06309 +1947345,0.309636,0.012444,1.04261,-3.75375,1.61875,-5.495,0.40866,0.19012,-0.52668,41.4877,-89.5063,181.285,13,1,33.37,1025.98,-112.651,0.09375,0.101562,1.05664,1,1,1,1,0,-111.889,0.19777,0.0581912,-111.974,8.06309 +1947355,-0.003355,-0.01586,1.032,-3.82375,1.0325,-6.965,0.4046,0.16492,-0.5509,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0957031,0.0976562,1.05859,1,1,1,1,0,-111.889,0.19777,0.0581912,-111.974,8.02441 +1947365,-0.003355,-0.01586,1.032,-3.82375,1.0325,-6.965,0.4046,0.16492,-0.5509,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0957031,0.0976562,1.05859,1,1,1,1,0,-111.889,0.19777,0.0581912,-111.974,8.02441 +1947375,-0.003355,-0.01586,1.032,-3.82375,1.0325,-6.965,0.4046,0.16492,-0.5509,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0957031,0.0976562,1.05859,1,1,1,1,0,-111.889,0.19777,0.0581912,-111.974,8.02441 +1947385,0.015311,-0.014213,1.03718,-3.43,-3.61375,-2.135,0.3745,0.11494,-0.53424,41.4877,-89.5063,181.272,13,1,33.38,1025.95,-112.393,0.0898438,0.0957031,1.05273,1,1,1,1,0,-111.892,0.197154,0.0130907,-111.977,8.05342 +1947395,0.015311,-0.014213,1.03718,-3.43,-3.61375,-2.135,0.3745,0.11494,-0.53424,41.4877,-89.5063,181.272,13,1,33.38,1025.95,-112.393,0.0898438,0.0957031,1.05273,1,1,1,1,0,-111.892,0.197154,0.0130907,-111.977,8.05342 +1947405,-0.012688,-0.017934,1.0295,-5.92375,4.27,11.27,0.40362,0.19068,-0.532,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0839844,0.0996094,1.05273,1,1,1,1,0,-111.892,0.197154,0.0130907,-111.977,8.05342 +1947415,-0.012688,-0.017934,1.0295,-5.92375,4.27,11.27,0.40362,0.19068,-0.532,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0839844,0.0996094,1.05273,1,1,1,1,0,-111.892,0.197154,0.0130907,-111.977,8.05342 +1947425,-0.012688,-0.017934,1.0295,-5.92375,4.27,11.27,0.40362,0.19068,-0.532,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0839844,0.0996094,1.05273,1,1,1,1,0,-111.892,0.197154,0.0130907,-111.977,8.05342 +1947435,-0.040382,-0.015128,1.0323,-4.15625,4.025,-7.49,0.40474,0.19488,-0.5292,41.4877,-89.5063,181.272,13,1,33.38,1025.94,-112.305,0.0839844,0.103516,1.05078,1,1,1,1,0,-111.895,0.194991,-0.00902473,-111.98,8.06309 +1947445,-0.040382,-0.015128,1.0323,-4.15625,4.025,-7.49,0.40474,0.19488,-0.5292,41.4877,-89.5063,181.272,13,1,33.38,1025.94,-112.305,0.0839844,0.103516,1.05078,1,1,1,1,0,-111.895,0.194991,-0.00902473,-111.98,8.06309 +1947455,-0.040382,-0.015128,1.0323,-4.15625,4.025,-7.49,0.40474,0.19488,-0.5292,41.4877,-89.5063,181.272,13,1,33.38,1025.94,-112.305,0.0839844,0.103516,1.05078,1,1,1,1,0,-111.895,0.194991,-0.00902473,-111.98,8.06309 +1947465,-0.28975,-0.048922,1.06335,-0.76125,2.2225,-20.93,0.37772,0.11522,-0.52962,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0917969,0.105469,1.05078,1,1,1,1,0,-111.895,0.194991,-0.00902473,-111.98,8.06309 +1947475,-0.28975,-0.048922,1.06335,-0.76125,2.2225,-20.93,0.37772,0.11522,-0.52962,41.4877,-89.5063,181.272,13,1,33.38,1025.98,-112.654,0.0917969,0.105469,1.05078,1,1,1,1,0,-111.895,0.194991,-0.00902473,-111.98,8.06309 +1947485,0.23302,-0.041602,1.01138,-6.125,4.9525,1.505,0.4095,0.19474,-0.5285,41.4877,-89.5063,181.272,13,1,33.39,1025.96,-112.484,0.09375,0.0976562,1.05664,1,1,1,1,0,-111.897,0.196007,0.0903203,-111.984,8.06309 +1947495,0.23302,-0.041602,1.01138,-6.125,4.9525,1.505,0.4095,0.19474,-0.5285,41.4877,-89.5063,181.272,13,1,33.39,1025.96,-112.484,0.09375,0.0976562,1.05664,1,1,1,1,0,-111.897,0.196007,0.0903203,-111.984,8.06309 +1947505,0.23302,-0.041602,1.01138,-6.125,4.9525,1.505,0.4095,0.19474,-0.5285,41.4877,-89.5063,181.272,13,1,33.39,1025.96,-112.484,0.09375,0.0976562,1.05664,1,1,1,1,0,-111.897,0.196007,0.0903203,-111.984,8.06309 +1947515,0.016165,-0.040382,1.03358,-2.2225,5.18,-14.2013,0.39354,0.13132,-0.5243,41.4877,-89.5063,181.272,13,1,33.38,1025.97,-112.567,0.0957031,0.0976562,1.0625,1,1,1,1,0,-111.897,0.196007,0.0903203,-111.984,8.04375 +1947525,0.016165,-0.040382,1.03358,-2.2225,5.18,-14.2013,0.39354,0.13132,-0.5243,41.4877,-89.5063,181.272,13,1,33.38,1025.97,-112.567,0.0957031,0.0976562,1.0625,1,1,1,1,0,-111.897,0.196007,0.0903203,-111.984,8.04375 +1947535,0.007137,-0.022143,1.03499,-3.7275,3.0275,-7.4725,0.36582,0.10178,-0.51744,41.4877,-89.5063,181.272,13,1,33.38,1025.94,-112.305,0.09375,0.0996094,1.05273,1,1,1,1,0,-111.898,0.197142,0.0337326,-111.985,8.05342 +1947545,0.007137,-0.022143,1.03499,-3.7275,3.0275,-7.4725,0.36582,0.10178,-0.51744,41.4877,-89.5063,181.272,13,1,33.38,1025.94,-112.305,0.09375,0.0996094,1.05273,1,1,1,1,0,-111.898,0.197142,0.0337326,-111.985,8.05342 +1947555,0.007137,-0.022143,1.03499,-3.7275,3.0275,-7.4725,0.36582,0.10178,-0.51744,41.4877,-89.5063,181.272,13,1,33.38,1025.94,-112.305,0.09375,0.0996094,1.05273,1,1,1,1,0,-111.898,0.197142,0.0337326,-111.985,8.05342 +1947565,0.053924,-0.037149,1.05243,-3.00125,1.18125,-8.18125,0.40026,0.19558,-0.51254,41.4877,-89.5063,181.254,13,1,33.39,1025.97,-112.57,0.09375,0.0996094,1.05469,1,1,1,1,0,-111.898,0.197142,0.0337326,-111.985,8.00508 +1947575,0.053924,-0.037149,1.05243,-3.00125,1.18125,-8.18125,0.40026,0.19558,-0.51254,41.4877,-89.5063,181.254,13,1,33.39,1025.97,-112.57,0.09375,0.0996094,1.05469,1,1,1,1,0,-111.898,0.197142,0.0337326,-111.985,8.00508 +1947585,0.053924,-0.037149,1.05243,-3.00125,1.18125,-8.18125,0.40026,0.19558,-0.51254,41.4877,-89.5063,181.254,13,1,33.39,1025.97,-112.57,0.09375,0.0996094,1.05469,1,1,1,1,0,-111.898,0.197142,0.0337326,-111.985,8.00508 +1947595,0.033977,-0.027877,1.0295,-3.675,2.415,-6.23,0.39676,0.1036,-0.51604,41.4877,-89.5063,181.254,13,1,33.39,1025.95,-112.396,0.0898438,0.0957031,1.05469,1,1,1,1,0,-111.895,0.197095,0.0103176,-111.986,8.03408 +1947605,0.033977,-0.027877,1.0295,-3.675,2.415,-6.23,0.39676,0.1036,-0.51604,41.4877,-89.5063,181.254,13,1,33.39,1025.95,-112.396,0.0898438,0.0957031,1.05469,1,1,1,1,0,-111.895,0.197095,0.0103176,-111.986,8.03408 +1947615,-0.297558,-0.043676,1.0312,-3.80625,1.88125,-3.5875,0.3955,0.10766,-0.50876,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0878906,0.0976562,1.05273,1,1,1,1,0,-111.895,0.197095,0.0103176,-111.986,8.02441 +1947625,-0.297558,-0.043676,1.0312,-3.80625,1.88125,-3.5875,0.3955,0.10766,-0.50876,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0878906,0.0976562,1.05273,1,1,1,1,0,-111.895,0.197095,0.0103176,-111.986,8.02441 +1947635,-0.297558,-0.043676,1.0312,-3.80625,1.88125,-3.5875,0.3955,0.10766,-0.50876,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0878906,0.0976562,1.05273,1,1,1,1,0,-111.895,0.197095,0.0103176,-111.986,8.02441 +1947645,0.024644,-0.026413,1.05091,-3.07125,2.1525,-7.72625,0.39802,0.10724,-0.56532,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0859375,0.0917969,1.05664,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,7.03828 +1947655,0.024644,-0.026413,1.05091,-3.07125,2.1525,-7.72625,0.39802,0.10724,-0.56532,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0859375,0.0917969,1.05664,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,7.03828 +1947665,-0.004392,-0.025559,1.04145,-3.73625,2.695,-9.96625,0.39466,0.17598,-0.5635,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0859375,0.0917969,1.05664,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,7.03828 +1947675,-0.004392,-0.025559,1.04145,-3.73625,2.695,-9.96625,0.39466,0.17598,-0.5635,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0898438,0.0957031,1.06055,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,7.98574 +1947685,-0.004392,-0.025559,1.04145,-3.73625,2.695,-9.96625,0.39466,0.17598,-0.5635,41.4877,-89.5063,181.254,13,1,33.39,1025.94,-112.309,0.0898438,0.0957031,1.06055,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,7.98574 +1947695,-0.009882,-0.002867,1.02754,-4.80375,8.75,-8.365,0.41006,0.10934,-0.5257,41.4877,-89.5063,181.254,13,1,33.37,1025.95,-112.389,0.0898438,0.0957031,1.06055,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,8.02441 +1947705,-0.009882,-0.002867,1.02754,-4.80375,8.75,-8.365,0.41006,0.10934,-0.5257,41.4877,-89.5063,181.254,13,1,33.37,1025.95,-112.389,0.0898438,0.0957031,1.06055,1,1,1,1,0,-111.888,0.199173,0.0773165,-111.983,8.02441 +1947715,-0.009882,-0.002867,1.02754,-4.80375,8.75,-8.365,0.41006,0.10934,-0.5257,41.4877,-89.5063,181.254,13,1,33.37,1025.95,-112.389,0.0878906,0.107422,1.05859,1,1,1,1,0,-111.885,0.202519,0.100086,-111.975,8.02441 +1947725,-0.009882,-0.002867,1.02754,-4.80375,8.75,-8.365,0.41006,0.10934,-0.5257,41.4877,-89.5063,181.254,13,1,33.37,1025.95,-112.389,0.0878906,0.107422,1.05859,1,1,1,1,0,-111.885,0.202519,0.100086,-111.975,8.02441 +1947735,-0.012932,-0.000183,1.02029,-0.28875,1.39125,-17.5438,0.40558,0.19012,-0.53018,41.4877,-89.5063,181.254,13,1,33.37,1025.94,-112.301,0.0839844,0.113281,1.0625,1,1,1,1,0,-111.885,0.202519,0.100086,-111.975,8.06309 +1947745,-0.012932,-0.000183,1.02029,-0.28875,1.39125,-17.5438,0.40558,0.19012,-0.53018,41.4877,-89.5063,181.254,13,1,33.37,1025.94,-112.301,0.0839844,0.113281,1.0625,1,1,1,1,0,-111.885,0.202519,0.100086,-111.975,8.06309 +1947755,-0.012932,-0.000183,1.02029,-0.28875,1.39125,-17.5438,0.40558,0.19012,-0.53018,41.4877,-89.5063,181.254,13,1,33.37,1025.94,-112.301,0.0839844,0.113281,1.0625,1,1,1,1,0,-111.885,0.202519,0.100086,-111.975,8.06309 +1947765,-0.009882,-0.001464,1.02407,-6.15125,2.82625,13.3263,0.37478,0.11158,-0.53536,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0820312,0.115234,1.06445,1,1,1,1,0,-111.879,0.204732,0.035121,-111.972,8.07275 +1947775,-0.009882,-0.001464,1.02407,-6.15125,2.82625,13.3263,0.37478,0.11158,-0.53536,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0820312,0.115234,1.06445,1,1,1,1,0,-111.879,0.204732,0.035121,-111.972,8.07275 +1947785,-0.019642,-0.015982,1.03438,-3.59625,0.56875,0.805,0.38402,0.1099,-0.53788,41.4877,-89.5063,181.235,13,1,33.38,1025.92,-112.13,0.0859375,0.107422,1.05469,1,1,1,1,0,-111.879,0.204732,0.035121,-111.972,8.07275 +1947795,-0.019642,-0.015982,1.03438,-3.59625,0.56875,0.805,0.38402,0.1099,-0.53788,41.4877,-89.5063,181.235,13,1,33.38,1025.92,-112.13,0.0859375,0.107422,1.05469,1,1,1,1,0,-111.879,0.204732,0.035121,-111.972,8.07275 +1947805,-0.019642,-0.015982,1.03438,-3.59625,0.56875,0.805,0.38402,0.1099,-0.53788,41.4877,-89.5063,181.235,13,1,33.38,1025.92,-112.13,0.0859375,0.107422,1.05469,1,1,1,1,0,-111.879,0.204732,0.035121,-111.972,8.07275 +1947815,-0.024095,-0.018971,1.02913,-3.605,-0.39375,-4.795,0.40866,0.19558,-0.53354,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0878906,0.109375,1.05273,1,1,1,1,0,-111.872,0.204127,-0.0233522,-111.965,8.07275 +1947825,-0.024095,-0.018971,1.02913,-3.605,-0.39375,-4.795,0.40866,0.19558,-0.53354,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0878906,0.109375,1.05273,1,1,1,1,0,-111.872,0.204127,-0.0233522,-111.965,8.07275 +1947835,-0.033367,-0.010187,1.03773,-3.6575,1.81125,-8.74125,0.38738,0.11214,-0.53186,41.4877,-89.5063,181.235,13,1,33.38,1025.91,-112.043,0.0898438,0.115234,1.04883,1,1,1,1,0,-111.872,0.204127,-0.0233522,-111.965,8.04375 +1947845,-0.033367,-0.010187,1.03773,-3.6575,1.81125,-8.74125,0.38738,0.11214,-0.53186,41.4877,-89.5063,181.235,13,1,33.38,1025.91,-112.043,0.0898438,0.115234,1.04883,1,1,1,1,0,-111.872,0.204127,-0.0233522,-111.965,8.04375 +1947855,-0.033367,-0.010187,1.03773,-3.6575,1.81125,-8.74125,0.38738,0.11214,-0.53186,41.4877,-89.5063,181.235,13,1,33.38,1025.91,-112.043,0.0898438,0.115234,1.04883,1,1,1,1,0,-111.872,0.204127,-0.0233522,-111.965,8.04375 +1947865,-0.032391,0.005002,1.0442,-3.82375,2.24,-6.1075,0.37576,0.11032,-0.5817,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.868,0.202515,0.00381153,-111.957,8.06309 +1947875,-0.032391,0.005002,1.0442,-3.82375,2.24,-6.1075,0.37576,0.11032,-0.5817,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.868,0.202515,0.00381153,-111.957,8.06309 +1947885,-0.032391,0.005002,1.0442,-3.82375,2.24,-6.1075,0.37576,0.11032,-0.5817,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0898438,0.123047,1.05078,1,1,1,1,0,-111.868,0.202515,0.00381153,-111.957,8.06309 +1947895,-0.025254,-0.00305,1.03218,-3.64,2.065,-4.41875,0.40852,0.1764,-0.5299,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0898438,0.125,1.05273,1,1,1,1,0,-111.868,0.202515,0.00381153,-111.957,8.06309 +1947905,-0.025254,-0.00305,1.03218,-3.64,2.065,-4.41875,0.40852,0.1764,-0.5299,41.4877,-89.5063,181.235,13,1,33.38,1025.94,-112.305,0.0898438,0.125,1.05273,1,1,1,1,0,-111.868,0.202515,0.00381153,-111.957,8.06309 +1947915,-0.024217,-0.005002,1.02761,-3.9025,2.45,-8.81125,0.39466,0.10304,-0.5278,41.4877,-89.5063,181.235,13,1,33.39,1025.95,-112.396,0.0878906,0.125,1.05273,1,1,1,1,0,-111.862,0.201496,-0.0100631,-111.955,8.04375 +1947925,-0.024217,-0.005002,1.02761,-3.9025,2.45,-8.81125,0.39466,0.10304,-0.5278,41.4877,-89.5063,181.235,13,1,33.39,1025.95,-112.396,0.0878906,0.125,1.05273,1,1,1,1,0,-111.862,0.201496,-0.0100631,-111.955,8.04375 +1947935,-0.024217,-0.005002,1.02761,-3.9025,2.45,-8.81125,0.39466,0.10304,-0.5278,41.4877,-89.5063,181.235,13,1,33.39,1025.95,-112.396,0.0878906,0.125,1.05273,1,1,1,1,0,-111.862,0.201496,-0.0100631,-111.955,8.04375 +1947945,-0.005673,-0.007564,1.03877,-2.63375,4.27875,-8.25125,0.3738,0.10584,-0.52094,41.4877,-89.5063,181.235,13,1,33.38,1025.92,-112.13,0.0800781,0.121094,1.05078,1,1,1,1,0,-111.862,0.201496,-0.0100631,-111.955,8.00508 +1947955,-0.005673,-0.007564,1.03877,-2.63375,4.27875,-8.25125,0.3738,0.10584,-0.52094,41.4877,-89.5063,181.235,13,1,33.38,1025.92,-112.13,0.0800781,0.121094,1.05078,1,1,1,1,0,-111.862,0.201496,-0.0100631,-111.955,8.00508 +1947965,-0.030805,-0.021289,1.04298,-3.3075,4.55,-13.0813,0.40082,0.17822,-0.5138,41.4877,-89.5063,181.235,13,1,33.39,1025.95,-112.396,0.0800781,0.121094,1.05078,1,1,1,1,0,-111.862,0.201496,-0.0100631,-111.955,8.00508 +1947975,-0.030805,-0.021289,1.04298,-3.3075,4.55,-13.0813,0.40082,0.17822,-0.5138,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0800781,0.121094,1.05273,1,1,1,1,0,-111.861,0.200015,0.00529319,-111.949,8.00508 +1947985,-0.030805,-0.021289,1.04298,-3.3075,4.55,-13.0813,0.40082,0.17822,-0.5138,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0800781,0.121094,1.05273,1,1,1,1,0,-111.861,0.200015,0.00529319,-111.949,8.00508 +1947995,-0.030805,-0.021289,1.04298,-3.3075,4.55,-13.0813,0.40082,0.17822,-0.5138,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0800781,0.121094,1.05273,1,1,1,1,0,-111.861,0.200015,0.00529319,-111.949,8.00508 +1948005,-0.011163,-0.012993,1.03493,-6.95625,2.9575,15.9862,0.39494,0.10304,-0.49364,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0820312,0.125,1.05273,1,1,1,1,0,-111.861,0.200015,0.00529319,-111.949,8.01475 +1948015,-0.011163,-0.012993,1.03493,-6.95625,2.9575,15.9862,0.39494,0.10304,-0.49364,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0820312,0.125,1.05273,1,1,1,1,0,-111.861,0.200015,0.00529319,-111.949,8.01475 +1948025,-0.001952,-0.02074,1.03157,-2.73,3.8325,-12.81,0.3934,0.10766,-0.50512,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0839844,0.126953,1.05664,1,1,1,1,0,-111.857,0.200519,0.0410319,-111.948,8.02441 +1948035,-0.001952,-0.02074,1.03157,-2.73,3.8325,-12.81,0.3934,0.10766,-0.50512,41.4877,-89.5063,181.212,13,1,33.39,1025.95,-112.396,0.0839844,0.126953,1.05664,1,1,1,1,0,-111.857,0.200519,0.0410319,-111.948,8.02441 +1948045,-0.007808,-0.015616,1.02907,-6.27375,6.7025,-2.84375,0.39606,0.10304,-0.50946,41.4877,-89.5063,181.212,13,1,33.39,1025.93,-112.221,0.0839844,0.121094,1.05664,1,1,1,1,0,-111.857,0.200519,0.0410319,-111.948,8.05342 +1948055,-0.007808,-0.015616,1.02907,-6.27375,6.7025,-2.84375,0.39606,0.10304,-0.50946,41.4877,-89.5063,181.212,13,1,33.39,1025.93,-112.221,0.0839844,0.121094,1.05664,1,1,1,1,0,-111.857,0.200519,0.0410319,-111.948,8.05342 +1948065,-0.007808,-0.015616,1.02907,-6.27375,6.7025,-2.84375,0.39606,0.10304,-0.50946,41.4877,-89.5063,181.212,13,1,33.39,1025.93,-112.221,0.0839844,0.121094,1.05664,1,1,1,1,0,-111.857,0.200519,0.0410319,-111.948,8.05342 +1948075,-0.012139,-0.009211,1.02602,-4.69875,6.2125,-0.28,0.3899,0.10556,-0.54614,41.4877,-89.5063,181.212,13,1,33.39,1025.93,-112.221,0.0820312,0.121094,1.05469,1,1,1,1,0,-111.855,0.20075,0.0278616,-111.944,8.01475 +1948085,-0.012139,-0.009211,1.02602,-4.69875,6.2125,-0.28,0.3899,0.10556,-0.54614,41.4877,-89.5063,181.212,13,1,33.39,1025.93,-112.221,0.0820312,0.121094,1.05469,1,1,1,1,0,-111.855,0.20075,0.0278616,-111.944,8.01475 +1948095,-0.412726,-0.049837,1.047,-4.305,1.40875,-3.255,0.37814,0.12026,-0.52892,41.4877,-89.5063,181.212,13,1,33.37,1025.96,-112.477,0.0820312,0.121094,1.05469,1,1,1,1,0,-111.855,0.20075,0.0278616,-111.944,8.01475 +1948105,-0.412726,-0.049837,1.047,-4.305,1.40875,-3.255,0.37814,0.12026,-0.52892,41.4877,-89.5063,181.212,13,1,33.37,1025.96,-112.477,0.0820312,0.121094,1.05469,1,1,1,1,0,-111.855,0.20075,0.0278616,-111.944,8.02441 +1948115,-0.412726,-0.049837,1.047,-4.305,1.40875,-3.255,0.37814,0.12026,-0.52892,41.4877,-89.5063,181.212,13,1,33.37,1025.96,-112.477,0.0820312,0.125,1.05664,1,1,1,1,0,-111.855,0.20075,0.0278616,-111.944,8.02441 +1948125,-0.412726,-0.049837,1.047,-4.305,1.40875,-3.255,0.37814,0.12026,-0.52892,41.4877,-89.5063,181.212,13,1,33.37,1025.96,-112.477,0.0820312,0.125,1.05664,1,1,1,1,0,-111.855,0.20075,0.0278616,-111.944,8.02441 +1948135,0.002806,-0.034648,1.02797,-3.73625,2.00375,-5.565,0.40992,0.18802,-0.52192,41.4877,-89.5063,181.212,13,1,33.37,1025.95,-112.389,0.0878906,0.128906,1.0625,1,1,1,1,0,-111.85,0.203972,0.111532,-111.942,8.07275 +1948145,0.002806,-0.034648,1.02797,-3.73625,2.00375,-5.565,0.40992,0.18802,-0.52192,41.4877,-89.5063,181.212,13,1,33.37,1025.95,-112.389,0.0878906,0.128906,1.0625,1,1,1,1,0,-111.85,0.203972,0.111532,-111.942,8.07275 +1948155,0.002806,-0.034648,1.02797,-3.73625,2.00375,-5.565,0.40992,0.18802,-0.52192,41.4877,-89.5063,181.212,13,1,33.37,1025.95,-112.389,0.0878906,0.128906,1.0625,1,1,1,1,0,-111.85,0.203972,0.111532,-111.942,8.07275 +1948165,0.006466,-0.037576,1.02212,-3.8325,2.1,-4.6025,0.38192,0.11046,-0.53186,41.4877,-89.5063,181.229,13,1,33.38,1025.93,-112.217,0.0917969,0.123047,1.06445,1,1,1,1,0,-111.85,0.203972,0.111532,-111.942,8.05342 +1948175,0.006466,-0.037576,1.02212,-3.8325,2.1,-4.6025,0.38192,0.11046,-0.53186,41.4877,-89.5063,181.229,13,1,33.38,1025.93,-112.217,0.0917969,0.123047,1.06445,1,1,1,1,0,-111.85,0.203972,0.111532,-111.942,8.05342 +1948185,0.006466,-0.037576,1.02212,-3.8325,2.1,-4.6025,0.38192,0.11046,-0.53186,41.4877,-89.5063,181.229,13,1,33.38,1025.93,-112.217,0.0917969,0.123047,1.06445,1,1,1,1,0,-111.85,0.203972,0.111532,-111.942,8.05342 +1948195,-0.013603,-0.024888,1.03419,-3.92,1.61875,-3.87625,0.378,0.1141,-0.5362,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0878906,0.107422,1.05664,1,1,1,1,0,-111.851,0.20609,0.0531276,-111.936,6.90293 +1948205,-0.013603,-0.024888,1.03419,-3.92,1.61875,-3.87625,0.378,0.1141,-0.5362,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0878906,0.107422,1.05664,1,1,1,1,0,-111.851,0.20609,0.0531276,-111.936,6.90293 +1948215,-0.019154,-0.009089,1.0262,-4.7775,3.465,13.0638,0.40978,0.19376,-0.52584,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0878906,0.111328,1.05664,1,1,1,1,0,-111.851,0.20609,0.0531276,-111.936,7.83105 +1948225,-0.019154,-0.009089,1.0262,-4.7775,3.465,13.0638,0.40978,0.19376,-0.52584,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0878906,0.111328,1.05664,1,1,1,1,0,-111.851,0.20609,0.0531276,-111.936,7.83105 +1948235,-0.019154,-0.009089,1.0262,-4.7775,3.465,13.0638,0.40978,0.19376,-0.52584,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0878906,0.111328,1.05664,1,1,1,1,0,-111.851,0.20609,0.0531276,-111.936,7.83105 +1948245,-0.014579,-0.020984,1.02876,-5.71375,2.6775,10.5262,0.40194,0.17654,-0.5411,41.4877,-89.5063,181.229,13,1,33.38,1025.95,-112.393,0.0898438,0.117188,1.05859,1,1,1,1,0,-111.85,0.208024,0.0804269,-111.936,8.01475 +1948255,-0.014579,-0.020984,1.02876,-5.71375,2.6775,10.5262,0.40194,0.17654,-0.5411,41.4877,-89.5063,181.229,13,1,33.38,1025.95,-112.393,0.0898438,0.117188,1.05859,1,1,1,1,0,-111.85,0.208024,0.0804269,-111.936,8.01475 +1948265,-0.311527,-0.031354,1.05664,-2.0825,4.095,-16.8438,0.3801,0.11452,-0.51954,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0859375,0.113281,1.06055,1,1,1,1,0,-111.85,0.208024,0.0804269,-111.936,8.05342 +1948275,-0.311527,-0.031354,1.05664,-2.0825,4.095,-16.8438,0.3801,0.11452,-0.51954,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0859375,0.113281,1.06055,1,1,1,1,0,-111.85,0.208024,0.0804269,-111.936,8.05342 +1948285,-0.311527,-0.031354,1.05664,-2.0825,4.095,-16.8438,0.3801,0.11452,-0.51954,41.4877,-89.5063,181.229,13,1,33.38,1025.96,-112.48,0.0859375,0.113281,1.06055,1,1,1,1,0,-111.85,0.208024,0.0804269,-111.936,8.05342 +1948295,-0.005612,-0.031049,1.02748,-6.1775,5.39875,8.8025,0.40768,0.17052,-0.53368,41.4877,-89.5063,181.229,13,1,33.38,1025.95,-112.393,0.0859375,0.103516,1.05664,1,1,1,1,0,-111.849,0.209179,0.03261,-111.935,8.05342 +1948305,-0.005612,-0.031049,1.02748,-6.1775,5.39875,8.8025,0.40768,0.17052,-0.53368,41.4877,-89.5063,181.229,13,1,33.38,1025.95,-112.393,0.0859375,0.103516,1.05664,1,1,1,1,0,-111.849,0.209179,0.03261,-111.935,8.05342 +1948315,-0.005612,-0.031049,1.02748,-6.1775,5.39875,8.8025,0.40768,0.17052,-0.53368,41.4877,-89.5063,181.229,13,1,33.38,1025.95,-112.393,0.0859375,0.103516,1.05664,1,1,1,1,0,-111.849,0.209179,0.03261,-111.935,8.05342 +1948325,0.001708,-0.027389,1.0234,-2.485,3.63125,-12.3025,0.3668,0.10556,-0.51646,41.4877,-89.5063,181.229,13,1,33.39,1025.95,-112.396,0.0898438,0.0957031,1.05469,1,1,1,1,0,-111.849,0.209179,0.03261,-111.935,8.03408 +1948335,0.001708,-0.027389,1.0234,-2.485,3.63125,-12.3025,0.3668,0.10556,-0.51646,41.4877,-89.5063,181.229,13,1,33.39,1025.95,-112.396,0.0898438,0.0957031,1.05469,1,1,1,1,0,-111.849,0.209179,0.03261,-111.935,8.03408 +1948345,-0.011224,-0.024461,1.03987,-3.85,5.57375,-5.9325,0.39494,0.1113,-0.49518,41.4877,-89.5063,181.229,13,1,33.39,1025.96,-112.484,0.0917969,0.101562,1.05078,1,1,1,1,0,-111.85,0.209356,0.0441318,-111.933,8.03408 +1948355,-0.011224,-0.024461,1.03987,-3.85,5.57375,-5.9325,0.39494,0.1113,-0.49518,41.4877,-89.5063,181.229,13,1,33.39,1025.96,-112.484,0.0917969,0.101562,1.05078,1,1,1,1,0,-111.85,0.209356,0.0441318,-111.933,8.03408 +1948365,-0.011224,-0.024461,1.03987,-3.85,5.57375,-5.9325,0.39494,0.1113,-0.49518,41.4877,-89.5063,181.229,13,1,33.39,1025.96,-112.484,0.0917969,0.101562,1.05078,1,1,1,1,0,-111.85,0.209356,0.0441318,-111.933,8.03408 +1948375,-0.033062,-0.025559,1.05072,-3.01,2.30125,-5.635,0.3955,0.1834,-0.5607,41.4877,-89.5063,181.236,13,1,33.39,1025.97,-112.57,0.0917969,0.103516,1.05664,1,1,1,1,0,-111.85,0.209356,0.0441318,-111.933,8.01475 +1948385,-0.033062,-0.025559,1.05072,-3.01,2.30125,-5.635,0.3955,0.1834,-0.5607,41.4877,-89.5063,181.236,13,1,33.39,1025.97,-112.57,0.0917969,0.103516,1.05664,1,1,1,1,0,-111.85,0.209356,0.0441318,-111.933,8.01475 +1948395,0.120719,-0.016287,1.0514,-4.025,2.5025,-6.13375,0.39844,0.13944,-0.53508,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0917969,0.103516,1.05664,1,1,1,1,0,-111.85,0.209356,0.0441318,-111.933,8.01475 +1948405,0.120719,-0.016287,1.0514,-4.025,2.5025,-6.13375,0.39844,0.13944,-0.53508,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0917969,0.101562,1.05859,1,1,1,1,0,-111.849,0.210308,0.0454512,-111.935,8.00508 +1948415,0.120719,-0.016287,1.0514,-4.025,2.5025,-6.13375,0.39844,0.13944,-0.53508,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0917969,0.101562,1.05859,1,1,1,1,0,-111.849,0.210308,0.0454512,-111.935,8.00508 +1948425,-0.089731,-0.022326,1.0714,-3.82375,1.89,-5.17125,0.39704,0.10948,-0.51562,41.4877,-89.5063,181.236,13,1,33.39,1025.95,-112.396,0.0878906,0.09375,1.05664,1,1,1,1,0,-111.849,0.210308,0.0454512,-111.935,8.04375 +1948435,-0.089731,-0.022326,1.0714,-3.82375,1.89,-5.17125,0.39704,0.10948,-0.51562,41.4877,-89.5063,181.236,13,1,33.39,1025.95,-112.396,0.0878906,0.09375,1.05664,1,1,1,1,0,-111.849,0.210308,0.0454512,-111.935,8.04375 +1948445,-0.089731,-0.022326,1.0714,-3.82375,1.89,-5.17125,0.39704,0.10948,-0.51562,41.4877,-89.5063,181.236,13,1,33.39,1025.95,-112.396,0.0878906,0.09375,1.05664,1,1,1,1,0,-111.849,0.210308,0.0454512,-111.935,8.04375 +1948455,0.008296,-0.026291,1.03596,-3.7275,1.61,-3.96375,0.3976,0.10654,-0.51296,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0898438,0.0917969,1.05859,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.04375 +1948465,0.008296,-0.026291,1.03596,-3.7275,1.61,-3.96375,0.3976,0.10654,-0.51296,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0898438,0.0917969,1.05859,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.04375 +1948475,0.009516,-0.030378,1.06866,-4.0425,1.2775,-7.175,0.40138,0.1092,-0.50624,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.05342 +1948485,0.009516,-0.030378,1.06866,-4.0425,1.2775,-7.175,0.40138,0.1092,-0.50624,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.05342 +1948495,0.009516,-0.030378,1.06866,-4.0425,1.2775,-7.175,0.40138,0.1092,-0.50624,41.4877,-89.5063,181.236,13,1,33.39,1025.96,-112.484,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.05342 +1948505,-0.030439,-0.017446,1.02425,-4.3925,-3.2375,3.28125,0.37758,0.11494,-0.5754,41.4877,-89.5063,181.236,13,1,33.37,1025.97,-112.563,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.04375 +1948515,-0.030439,-0.017446,1.02425,-4.3925,-3.2375,3.28125,0.37758,0.11494,-0.5754,41.4877,-89.5063,181.236,13,1,33.37,1025.97,-112.563,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.845,0.209865,0.0107192,-111.933,8.04375 +1948525,-0.030439,-0.017446,1.02425,-4.3925,-3.2375,3.28125,0.37758,0.11494,-0.5754,41.4877,-89.5063,181.236,13,1,33.37,1025.97,-112.563,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.843,0.207919,-0.00921728,-111.929,8.04375 +1948535,-0.030439,-0.017446,1.02425,-4.3925,-3.2375,3.28125,0.37758,0.11494,-0.5754,41.4877,-89.5063,181.236,13,1,33.37,1025.97,-112.563,0.0898438,0.101562,1.05273,1,1,1,1,0,-111.843,0.207919,-0.00921728,-111.929,8.04375 +1948545,0.014701,-0.022143,1.05597,-0.97125,0.5425,-21.77,0.40824,0.18032,-0.52584,41.4877,-89.5063,181.236,13,1,33.37,1025.95,-112.389,0.0898438,0.0976562,1.05078,1,1,1,1,0,-111.843,0.207919,-0.00921728,-111.929,8.05342 +1948555,0.014701,-0.022143,1.05597,-0.97125,0.5425,-21.77,0.40824,0.18032,-0.52584,41.4877,-89.5063,181.236,13,1,33.37,1025.95,-112.389,0.0898438,0.0976562,1.05078,1,1,1,1,0,-111.843,0.207919,-0.00921728,-111.929,8.05342 +1948565,0.248392,-0.921466,1.9986,-1.6275,-0.28,-11.6988,0.3997,0.14196,-0.5404,41.4877,-89.5063,181.224,13,1,33.38,1026.01,-112.917,0.0898438,0.0917969,1.09961,1,1,1,1,0,-111.843,0.207919,-0.00921728,-111.929,8.07275 +1948575,0.248392,-0.921466,1.9986,-1.6275,-0.28,-11.6988,0.3997,0.14196,-0.5404,41.4877,-89.5063,181.224,13,1,33.38,1026.01,-112.917,0.0898438,0.0917969,1.09961,1,1,1,1,0,-111.836,2.48238,70.4669,-111.928,8.07275 +1948585,0.248392,-0.921466,1.9986,-1.6275,-0.28,-11.6988,0.3997,0.14196,-0.5404,41.4877,-89.5063,181.224,13,1,33.38,1026.01,-112.917,0.0898438,0.0917969,1.09961,1,1,1,1,0,-111.836,2.48238,70.4669,-111.928,8.07275 +1948595,-0.206668,0.762256,1.9986,-6.50125,5.215,17.0362,0.37954,0.12684,-0.53382,41.4877,-89.5063,181.224,13,1,33.38,1026.3,-115.45,-0.300781,0.232422,9.19141,1,1,1,1,0,-111.836,2.48238,70.4669,-111.928,8.05342 +1948605,-0.206668,0.762256,1.9986,-6.50125,5.215,17.0362,0.37954,0.12684,-0.53382,41.4877,-89.5063,181.224,13,1,33.38,1026.3,-115.45,-0.300781,0.232422,9.19141,1,1,1,1,0,-111.836,2.48238,70.4669,-111.928,8.05342 +1948615,-0.206668,0.762256,1.9986,-6.50125,5.215,17.0362,0.37954,0.12684,-0.53382,41.4877,-89.5063,181.224,13,1,33.38,1026.3,-115.45,-0.300781,0.232422,9.19141,1,1,1,1,0,-111.836,2.48238,70.4669,-111.928,8.05342 +1948625,1.01223,-1.4177,1.9986,-5.04,-2.49375,5.32875,0.40432,0.20664,-0.53032,41.4877,-89.5063,181.224,13,1,33.38,1026.23,-114.839,-0.4375,-0.253906,12.0195,2,2,2,2,0,-111.58,7.5485,113.276,-111.239,8.07275 +1948635,1.01223,-1.4177,1.9986,-5.04,-2.49375,5.32875,0.40432,0.20664,-0.53032,41.4877,-89.5063,181.224,13,1,33.38,1026.23,-114.839,-0.4375,-0.253906,12.0195,2,2,2,2,0,-111.58,7.5485,113.276,-111.239,8.07275 +1948645,-0.332389,1.84458,1.9986,-3.57875,-1.05,-4.2175,0.39956,0.1281,-0.532,41.4877,-89.5063,181.224,13,1,33.38,1025.86,-111.606,0.412109,-0.265625,13.1699,2,2,2,2,0,-111.58,7.5485,113.276,-111.239,8.07275 +1948655,-0.332389,1.84458,1.9986,-3.57875,-1.05,-4.2175,0.39956,0.1281,-0.532,41.4877,-89.5063,181.224,13,1,33.38,1025.86,-111.606,0.412109,-0.265625,13.1699,2,2,2,2,0,-111.58,7.5485,113.276,-111.239,8.07275 +1948665,-0.332389,1.84458,1.9986,-3.57875,-1.05,-4.2175,0.39956,0.1281,-0.532,41.4877,-89.5063,181.224,13,1,33.38,1025.86,-111.606,0.412109,-0.265625,13.1699,2,2,2,2,0,-111.58,7.5485,113.276,-111.239,8.07275 +1948675,-0.211548,1.9986,1.9986,-3.78875,1.07625,-4.06875,0.38682,0.13888,-0.539,41.4877,-89.5063,181.224,13,1,33.39,1025.77,-110.823,-0.935547,-0.132812,14.2422,2,2,2,2,0,-110.996,14.116,130.139,-107.878,8.04375 +1948685,-0.211548,1.9986,1.9986,-3.78875,1.07625,-4.06875,0.38682,0.13888,-0.539,41.4877,-89.5063,181.224,13,1,33.39,1025.77,-110.823,-0.935547,-0.132812,14.2422,2,2,2,2,0,-110.996,14.116,130.139,-107.878,8.04375 +1948695,1.52,0.176168,1.9986,-4.0425,2.23125,-3.68375,0.39662,0.13412,-0.57722,41.4877,-89.5063,181.224,13,1,33.39,1025.66,-109.861,-0.935547,-0.132812,14.2422,2,2,2,2,0,-110.996,14.116,130.139,-107.878,8.04375 +1948705,1.52,0.176168,1.9986,-4.0425,2.23125,-3.68375,0.39662,0.13412,-0.57722,41.4877,-89.5063,181.224,13,1,33.39,1025.66,-109.861,-1.36523,0.107422,14.5391,2,2,2,2,0,-110.996,14.116,130.139,-107.878,8.04375 +1948715,1.52,0.176168,1.9986,-4.0425,2.23125,-3.68375,0.39662,0.13412,-0.57722,41.4877,-89.5063,181.224,13,1,33.39,1025.66,-109.861,-1.36523,0.107422,14.5391,2,2,2,2,0,-110.996,14.116,130.139,-107.878,8.04375 +1948725,-0.654896,-1.88435,1.9986,-3.56125,2.1875,-7.07875,0.37786,0.15008,-0.5572,41.4877,-89.5063,181.224,13,1,33.39,1025.68,-110.035,-1.69141,-0.605469,14.8184,2,2,2,2,0,-110.005,21.2433,136.713,-99.4021,8.05342 +1948735,-0.654896,-1.88435,1.9986,-3.56125,2.1875,-7.07875,0.37786,0.15008,-0.5572,41.4877,-89.5063,181.224,13,1,33.39,1025.68,-110.035,-1.69141,-0.605469,14.8184,2,2,2,2,0,-110.005,21.2433,136.713,-99.4021,8.05342 +1948745,-0.654896,-1.88435,1.9986,-3.56125,2.1875,-7.07875,0.37786,0.15008,-0.5572,41.4877,-89.5063,181.224,13,1,33.39,1025.68,-110.035,-1.69141,-0.605469,14.8184,2,2,2,2,0,-110.005,21.2433,136.713,-99.4021,8.05342 +1948755,-0.707905,-0.900787,1.9986,-2.905,2.35375,-9.14375,0.35882,0.16212,-0.56336,41.4877,-89.5063,181.224,13,1,33.39,1025.25,-106.275,1.05859,0.185547,15.0742,2,2,2,2,0,-110.005,21.2433,136.713,-99.4021,8.00508 +1948765,-0.707905,-0.900787,1.9986,-2.905,2.35375,-9.14375,0.35882,0.16212,-0.56336,41.4877,-89.5063,181.224,13,1,33.39,1025.25,-106.275,1.05859,0.185547,15.0742,2,2,2,2,0,-110.005,21.2433,136.713,-99.4021,8.00508 +1948775,0.353312,-0.127368,1.9986,-3.1675,-4.655,-0.60375,0.34132,0.17892,-0.5558,41.4877,-89.5063,181.238,13,1,33.39,1025.61,-109.424,2.14258,0.876953,15.0977,2,2,2,2,0,-108.725,28.4729,140.409,-85.003,8.05342 +1948785,0.353312,-0.127368,1.9986,-3.1675,-4.655,-0.60375,0.34132,0.17892,-0.5558,41.4877,-89.5063,181.238,13,1,33.39,1025.61,-109.424,2.14258,0.876953,15.0977,2,2,2,2,0,-108.725,28.4729,140.409,-85.003,8.05342 +1948795,0.353312,-0.127368,1.9986,-3.1675,-4.655,-0.60375,0.34132,0.17892,-0.5558,41.4877,-89.5063,181.238,13,1,33.39,1025.61,-109.424,2.14258,0.876953,15.0977,2,2,2,2,0,-108.725,28.4729,140.409,-85.003,8.05342 +1948805,0.036234,0.862784,1.9986,-3.045,-3.15,-6.7025,0.3262,0.17682,-0.55664,41.4877,-89.5063,181.238,13,1,33.39,1025.57,-109.073,-1.12109,-0.71875,15.4141,2,2,2,2,0,-108.725,28.4729,140.409,-85.003,8.04375 +1948815,0.036234,0.862784,1.9986,-3.045,-3.15,-6.7025,0.3262,0.17682,-0.55664,41.4877,-89.5063,181.238,13,1,33.39,1025.57,-109.073,-1.12109,-0.71875,15.4141,2,2,2,2,0,-108.725,28.4729,140.409,-85.003,8.04375 +1948825,0.036234,0.862784,1.9986,-3.045,-3.15,-6.7025,0.3262,0.17682,-0.55664,41.4877,-89.5063,181.238,13,1,33.39,1025.3,-106.713,-1.12109,-0.71875,15.4141,2,2,2,2,0,-108.725,28.4729,140.409,-85.003,8.04375 +1948835,-0.052521,-1.9986,1.9986,-4.4975,5.6875,-2.2575,0.30338,0.19586,-0.55412,41.4877,-89.5063,181.238,13,1,33.39,1025.3,-106.713,-2.16211,-0.785156,15.5273,2,2,2,2,0,-107.036,35.9559,141.686,-64.7441,8.02441 +1948845,-0.052521,-1.9986,1.9986,-4.4975,5.6875,-2.2575,0.30338,0.19586,-0.55412,41.4877,-89.5063,181.238,13,1,33.39,1025.3,-106.713,-2.16211,-0.785156,15.5273,2,2,2,2,0,-107.036,35.9559,141.686,-64.7441,8.02441 +1948855,-0.358924,-1.40221,1.9986,-5.22375,5.27625,2.23125,0.27538,0.21098,-0.55174,41.4877,-89.5063,181.238,13,1,33.39,1025.38,-107.413,-0.320312,0.78125,15.5117,2,2,2,2,0,-107.036,35.9559,141.686,-64.7441,8.04375 +1948865,-0.358924,-1.40221,1.9986,-5.22375,5.27625,2.23125,0.27538,0.21098,-0.55174,41.4877,-89.5063,181.238,13,1,33.39,1025.38,-107.413,-0.320312,0.78125,15.5117,2,2,2,2,0,-107.036,35.9559,141.686,-64.7441,8.04375 +1948875,-0.358924,-1.40221,1.9986,-5.22375,5.27625,2.23125,0.27538,0.21098,-0.55174,41.4877,-89.5063,181.238,13,1,33.39,1025.38,-107.413,-0.320312,0.78125,15.5117,2,2,2,2,0,-107.036,35.9559,141.686,-64.7441,8.04375 +1948885,0.12261,1.35664,1.9986,-3.6575,-3.94625,2.5025,0.2653,0.20846,-0.5502,41.4877,-89.5063,181.238,13,1,33.4,1025.31,-106.804,-0.925781,-0.988281,15.752,3,3,3,3,0,-104.43,45.2604,143.963,-38.1885,8.05342 +1948895,0.12261,1.35664,1.9986,-3.6575,-3.94625,2.5025,0.2653,0.20846,-0.5502,41.4877,-89.5063,181.238,13,1,33.4,1025.31,-106.804,-0.925781,-0.988281,15.752,3,3,3,3,0,-104.43,45.2604,143.963,-38.1885,8.05342 +1948905,-0.285846,-1.6944,1.9986,-5.39,0.72625,6.1075,0.2352,0.224,-0.5621,41.4877,-89.5063,181.238,13,1,33.38,1024.8,-102.335,-0.925781,-0.988281,15.752,3,3,3,3,0,-104.43,45.2604,143.963,-38.1885,8.05342 +1948915,-0.285846,-1.6944,1.9986,-5.39,0.72625,6.1075,0.2352,0.224,-0.5621,41.4877,-89.5063,181.238,13,1,33.38,1024.8,-102.335,-0.925781,-0.988281,15.752,3,3,3,3,0,-104.43,45.2604,143.963,-38.1885,8.05342 +1948925,-0.285846,-1.6944,1.9986,-5.39,0.72625,6.1075,0.2352,0.224,-0.5621,41.4877,-89.5063,181.238,13,1,33.38,1024.8,-102.335,-0.662109,0.226562,15.7246,3,3,3,3,0,-104.43,45.2604,143.963,-38.1885,8.05342 +1948935,-0.285846,-1.6944,1.9986,-5.39,0.72625,6.1075,0.2352,0.224,-0.5621,41.4877,-89.5063,181.238,13,1,33.38,1024.8,-102.335,-0.662109,0.226562,15.7246,3,3,3,3,0,-104.43,45.2604,143.963,-38.1885,8.05342 +1948945,0.964471,0.516304,1.9986,-4.36625,2.72125,-1.07625,0.22638,0.21994,-0.5684,41.4877,-89.5063,181.238,13,1,33.38,1025.05,-104.522,0.537109,1.36719,15.6719,3,3,3,3,0,-101.845,52.8905,143.992,3.10188,8.07275 +1948955,0.964471,0.516304,1.9986,-4.36625,2.72125,-1.07625,0.22638,0.21994,-0.5684,41.4877,-89.5063,181.238,13,1,33.38,1025.05,-104.522,0.537109,1.36719,15.6719,3,3,3,3,0,-101.845,52.8905,143.992,3.10188,8.07275 +1948965,0.964471,0.516304,1.9986,-4.36625,2.72125,-1.07625,0.22638,0.21994,-0.5684,41.4877,-89.5063,181.238,13,1,33.38,1025.05,-104.522,0.537109,1.36719,15.6719,3,3,3,3,0,-101.845,52.8905,143.992,3.10188,8.07275 +1948975,0.726937,0.845216,1.9986,-3.80625,1.98625,-6.37,0.21672,0.2177,-0.55958,41.4877,-89.5063,181.319,12,1,33.38,1024.78,-102.161,0.720703,0.464844,15.7305,3,3,3,3,0,-101.845,52.8905,143.992,3.10188,8.08242 +1948985,0.726937,0.845216,1.9986,-3.80625,1.98625,-6.37,0.21672,0.2177,-0.55958,41.4877,-89.5063,181.319,12,1,33.38,1024.78,-102.161,0.720703,0.464844,15.7305,3,3,3,3,0,-101.845,52.8905,143.992,3.10188,8.08242 +1948995,-0.873642,-1.99256,1.9986,-3.64,2.58125,-5.74875,0.19628,0.21252,-0.55986,41.4877,-89.5063,181.319,12,1,33.38,1024.45,-99.2717,0.720703,0.464844,15.7305,3,3,3,3,0,-101.845,52.8905,143.992,3.10188,8.08242 +1949005,-0.873642,-1.99256,1.9986,-3.64,2.58125,-5.74875,0.19628,0.21252,-0.55986,41.4877,-89.5063,181.319,12,1,33.38,1024.45,-99.2717,-0.474609,0.0722656,15.8359,3,3,3,3,0,-98.72,60.8448,144.916,43.5772,8.06309 +1949015,-0.873642,-1.99256,1.9986,-3.64,2.58125,-5.74875,0.19628,0.21252,-0.55986,41.4877,-89.5063,181.319,12,1,33.38,1024.45,-99.2717,-0.474609,0.0722656,15.8359,3,3,3,3,0,-98.72,60.8448,144.916,43.5772,8.06309 +1949025,1.13064,-0.520818,1.9986,-4.97,2.89625,2.66875,0.18494,0.21322,-0.5663,41.4877,-89.5063,181.319,12,1,33.38,1024.27,-97.6974,-0.474609,0.0722656,15.8359,3,3,3,3,0,-98.72,60.8448,144.916,43.5772,8.07275 +1949035,1.13064,-0.520818,1.9986,-4.97,2.89625,2.66875,0.18494,0.21322,-0.5663,41.4877,-89.5063,181.319,12,1,33.38,1024.27,-97.6974,0.0273438,1.11914,15.8105,3,3,3,3,0,-98.72,60.8448,144.916,43.5772,8.07275 +1949045,1.13064,-0.520818,1.9986,-4.97,2.89625,2.66875,0.18494,0.21322,-0.5663,41.4877,-89.5063,181.319,12,1,33.38,1024.27,-97.6974,0.0273438,1.11914,15.8105,3,3,3,3,0,-98.72,60.8448,144.916,43.5772,8.07275 +1949055,0.116022,0.680028,1.9986,-3.92,-4.0425,0.30625,0.17668,0.20384,-0.56112,41.4877,-89.5063,181.319,12,1,33.39,1024.06,-95.8604,0.480469,0.537109,15.8105,3,3,3,3,0,-95.4239,68.2583,145.575,91.8022,8.06309 +1949065,0.116022,0.680028,1.9986,-3.92,-4.0425,0.30625,0.17668,0.20384,-0.56112,41.4877,-89.5063,181.319,12,1,33.39,1024.06,-95.8604,0.480469,0.537109,15.8105,3,3,3,3,0,-95.4239,68.2583,145.575,91.8022,8.06309 +1949075,-0.445788,-0.896456,1.9986,-3.8675,7.95375,-8.995,0.1617,0.19418,-0.56434,41.4877,-89.5063,181.319,12,1,33.39,1023.96,-94.9846,-0.205078,0.105469,15.9004,3,3,3,3,0,-95.4239,68.2583,145.575,91.8022,8.06309 +1949085,-0.445788,-0.896456,1.9986,-3.8675,7.95375,-8.995,0.1617,0.19418,-0.56434,41.4877,-89.5063,181.319,12,1,33.39,1023.96,-94.9846,-0.205078,0.105469,15.9004,3,3,3,3,0,-95.4239,68.2583,145.575,91.8022,8.06309 +1949095,-0.445788,-0.896456,1.9986,-3.8675,7.95375,-8.995,0.1617,0.19418,-0.56434,41.4877,-89.5063,181.319,12,1,33.39,1023.96,-94.9846,-0.205078,0.105469,15.9004,3,3,3,3,0,-95.4239,68.2583,145.575,91.8022,8.06309 +1949105,1.96914,-0.519964,1.9986,-5.18875,3.36875,9.66875,0.16072,0.18368,-0.56518,41.4877,-89.5063,181.319,12,1,33.39,1023.76,-93.2328,-0.0839844,0.916016,15.8848,3,3,3,3,0,-91.605,75.9811,145.755,142.302,8.04375 +1949115,1.96914,-0.519964,1.9986,-5.18875,3.36875,9.66875,0.16072,0.18368,-0.56518,41.4877,-89.5063,181.319,12,1,33.39,1023.76,-93.2328,-0.0839844,0.916016,15.8848,3,3,3,3,0,-91.605,75.9811,145.755,142.302,8.04375 +1949125,1.96914,-0.519964,1.9986,-5.18875,3.36875,9.66875,0.16072,0.18368,-0.56518,41.4877,-89.5063,181.319,12,1,33.39,1023.76,-93.2328,-0.0839844,0.916016,15.8848,3,3,3,3,0,-91.605,75.9811,145.755,142.302,8.04375 +1949135,-0.054473,0.353068,1.9986,-3.8325,-2.59,-4.15625,0.14014,0.16744,-0.55328,41.4877,-89.5063,181.319,12,1,33.39,1023.52,-91.1304,0.261719,-0.279297,15.9121,3,3,3,3,0,-91.605,75.9811,145.755,142.302,8.05342 +1949145,-0.054473,0.353068,1.9986,-3.8325,-2.59,-4.15625,0.14014,0.16744,-0.55328,41.4877,-89.5063,181.319,12,1,33.39,1023.52,-91.1304,0.261719,-0.279297,15.9121,3,3,3,3,0,-91.605,75.9811,145.755,142.302,8.05342 +1949155,-0.497089,-0.75945,1.9986,-4.8825,6.125,-4.08625,0.13524,0.15932,-0.55762,41.4877,-89.5063,181.319,12,1,33.39,1023.42,-90.2553,-0.0371094,0.123047,15.9297,3,3,3,3,0,-87.4127,83.7051,145.86,200.232,8.06309 +1949165,-0.497089,-0.75945,1.9986,-4.8825,6.125,-4.08625,0.13524,0.15932,-0.55762,41.4877,-89.5063,181.319,12,1,33.39,1023.42,-90.2553,-0.0371094,0.123047,15.9297,3,3,3,3,0,-87.4127,83.7051,145.86,200.232,8.06309 +1949175,-0.497089,-0.75945,1.9986,-4.8825,6.125,-4.08625,0.13524,0.15932,-0.55762,41.4877,-89.5063,181.319,12,1,33.39,1023.42,-90.2553,-0.0371094,0.123047,15.9297,3,3,3,3,0,-87.4127,83.7051,145.86,200.232,8.06309 +1949185,0.928908,-1.07909,1.9986,-4.0425,0.41125,-3.9375,0.13202,0.1498,-0.55076,41.4877,-89.5063,181.433,11,1,33.39,1023.28,-89.0275,-0.148438,0.742188,15.9219,3,3,3,3,0,-87.4127,83.7051,145.86,200.232,8.04375 +1949195,0.928908,-1.07909,1.9986,-4.0425,0.41125,-3.9375,0.13202,0.1498,-0.55076,41.4877,-89.5063,181.433,11,1,33.39,1023.28,-89.0275,-0.148438,0.742188,15.9219,3,3,3,3,0,-87.4127,83.7051,145.86,200.232,8.04375 +1949205,0.335866,-0.318359,1.9986,-3.605,2.98375,-5.88,0.12908,0.13636,-0.5572,41.4877,-89.5063,181.433,11,1,33.39,1022.9,-85.697,0.230469,-0.123047,15.9355,3,3,3,3,0,-82.813,91.4381,146.076,263.686,8.02441 +1949215,0.335866,-0.318359,1.9986,-3.605,2.98375,-5.88,0.12908,0.13636,-0.5572,41.4877,-89.5063,181.433,11,1,33.39,1022.9,-85.697,0.230469,-0.123047,15.9355,3,3,3,3,0,-82.813,91.4381,146.076,263.686,8.02441 +1949225,0.335866,-0.318359,1.9986,-3.605,2.98375,-5.88,0.12908,0.13636,-0.5572,41.4877,-89.5063,181.433,11,1,33.39,1022.9,-85.697,0.230469,-0.123047,15.9355,3,3,3,3,0,-82.813,91.4381,146.076,263.686,8.02441 +1949235,-0.496296,-0.174826,1.9986,-3.43,1.6625,-8.4175,0.12782,0.12712,-0.5628,41.4877,-89.5063,181.433,11,1,33.4,1022.77,-84.5604,0.0273438,0.189453,15.9453,3,3,3,3,0,-82.813,91.4381,146.076,263.686,8.01475 +1949245,-0.496296,-0.174826,1.9986,-3.43,1.6625,-8.4175,0.12782,0.12712,-0.5628,41.4877,-89.5063,181.433,11,1,33.4,1022.77,-84.5604,0.0273438,0.189453,15.9453,3,3,3,3,0,-82.813,91.4381,146.076,263.686,8.01475 +1949255,-0.496296,-0.174826,1.9986,-3.43,1.6625,-8.4175,0.12782,0.12712,-0.5628,41.4877,-89.5063,181.433,11,1,33.4,1022.77,-84.5604,0.0273438,0.189453,15.9453,3,3,3,3,0,-82.813,91.4381,146.076,263.686,8.01475 +1949265,1.06579,-0.865163,1.9986,-3.66625,1.61875,-5.0925,0.13174,0.11774,-0.55664,41.4877,-89.5063,181.433,11,1,33.4,1022.56,-82.7186,-0.0957031,0.539062,15.9375,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.04375 +1949275,1.06579,-0.865163,1.9986,-3.66625,1.61875,-5.0925,0.13174,0.11774,-0.55664,41.4877,-89.5063,181.433,11,1,33.4,1022.56,-82.7186,-0.0957031,0.539062,15.9375,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.04375 +1949285,0.224053,-0.263642,1.9986,-4.9525,3.325,-2.30125,0.1316,0.10542,-0.56322,41.4877,-89.5063,181.433,11,1,33.4,1022.13,-78.9469,0.0390625,0.244141,15.9609,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.04375 +1949295,0.224053,-0.263642,1.9986,-4.9525,3.325,-2.30125,0.1316,0.10542,-0.56322,41.4877,-89.5063,181.433,11,1,33.4,1022.13,-78.9469,0.0390625,0.244141,15.9609,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.04375 +1949305,0.224053,-0.263642,1.9986,-4.9525,3.325,-2.30125,0.1316,0.10542,-0.56322,41.4877,-89.5063,181.433,11,1,33.4,1022.13,-78.9469,0.0390625,0.244141,15.9609,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.04375 +1949315,0.099796,-0.400343,1.9986,-5.85375,4.31375,4.68125,0.15134,0.09632,-0.58772,41.4877,-89.5063,181.433,11,1,33.38,1021.75,-75.6077,0.0390625,0.244141,15.9609,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.03408 +1949325,0.099796,-0.400343,1.9986,-5.85375,4.31375,4.68125,0.15134,0.09632,-0.58772,41.4877,-89.5063,181.433,11,1,33.38,1021.75,-75.6077,0.0390625,0.244141,15.9609,3,3,3,3,0,-76.2039,101.661,146.24,332.165,8.03408 +1949335,0.099796,-0.400343,1.9986,-5.85375,4.31375,4.68125,0.15134,0.09632,-0.58772,41.4877,-89.5063,181.433,11,1,33.38,1021.75,-75.6077,-0.113281,0.482422,15.9805,3,3,3,3,0,-70.8528,109.116,146.272,430.481,8.03408 +1949345,0.099796,-0.400343,1.9986,-5.85375,4.31375,4.68125,0.15134,0.09632,-0.58772,41.4877,-89.5063,181.433,11,1,33.38,1021.75,-75.6077,-0.113281,0.482422,15.9805,3,3,3,3,0,-70.8528,109.116,146.272,430.481,8.03408 +1949355,-0.130418,-0.135908,1.9986,-2.28375,2.52,-12.5125,0.15288,0.09072,-0.5943,41.4877,-89.5063,181.433,11,1,33.38,1021.34,-72.0082,0.0410156,0.355469,15.9629,3,3,3,3,0,-70.8528,109.116,146.272,430.481,8.07275 +1949365,-0.130418,-0.135908,1.9986,-2.28375,2.52,-12.5125,0.15288,0.09072,-0.5943,41.4877,-89.5063,181.433,11,1,33.38,1021.34,-72.0082,0.0410156,0.355469,15.9629,3,3,3,3,0,-70.8528,109.116,146.272,430.481,8.07275 +1949375,-0.23241,0.34465,1.9986,-5.81,3.26375,4.935,0.1617,0.07728,-0.59822,41.4877,-89.5063,181.594,10,1,33.38,1021.02,-69.1987,0.228516,0.167969,15.9531,3,3,3,3,0,-70.8528,109.116,146.272,430.481,8.07275 +1949385,-0.23241,0.34465,1.9986,-5.81,3.26375,4.935,0.1617,0.07728,-0.59822,41.4877,-89.5063,181.594,10,1,33.38,1021.02,-69.1987,0.228516,0.167969,15.9531,3,3,3,3,0,-65.0903,116.581,146.377,507.514,8.07275 +1949395,-0.23241,0.34465,1.9986,-5.81,3.26375,4.935,0.1617,0.07728,-0.59822,41.4877,-89.5063,181.594,10,1,33.38,1021.02,-69.1987,0.228516,0.167969,15.9531,3,3,3,3,0,-65.0903,116.581,146.377,507.514,8.07275 +1949405,0.084912,-0.665937,1.9986,-5.83625,1.9775,8.085,0.16702,0.0665,-0.59948,41.4877,-89.5063,181.594,10,1,33.38,1020.48,-64.4548,-0.046875,0.417969,15.9746,3,3,3,3,0,-65.0903,116.581,146.377,507.514,8.04375 +1949415,0.084912,-0.665937,1.9986,-5.83625,1.9775,8.085,0.16702,0.0665,-0.59948,41.4877,-89.5063,181.594,10,1,33.38,1020.48,-64.4548,-0.046875,0.417969,15.9746,3,3,3,3,0,-65.0903,116.581,146.377,507.514,8.04375 +1949425,0.084912,-0.665937,1.9986,-5.83625,1.9775,8.085,0.16702,0.0665,-0.59948,41.4877,-89.5063,181.594,10,1,33.38,1020.48,-64.4548,-0.046875,0.417969,15.9746,3,3,3,3,0,-65.0903,116.581,146.377,507.514,8.04375 +1949435,0.073627,-0.460672,1.9986,-2.52,2.2575,-14.6388,0.1673,0.05516,-0.60508,41.4877,-89.5063,181.594,10,1,33.39,1019.98,-60.0618,0.0800781,0.308594,15.9941,3,3,3,3,0,-58.665,124.368,146.95,588.821,8.07275 +1949445,0.073627,-0.460672,1.9986,-2.52,2.2575,-14.6388,0.1673,0.05516,-0.60508,41.4877,-89.5063,181.594,10,1,33.39,1019.98,-60.0618,0.0800781,0.308594,15.9941,3,3,3,3,0,-58.665,124.368,146.95,588.821,8.07275 +1949455,-0.24217,0.201605,1.9986,-4.7075,3.10625,-3.31625,0.1722,0.04438,-0.60844,41.4877,-89.5063,181.594,10,1,33.39,1019.48,-55.6646,0.0722656,0.320312,16.0391,3,3,3,3,0,-58.665,124.368,146.95,588.821,8.05342 +1949465,-0.24217,0.201605,1.9986,-4.7075,3.10625,-3.31625,0.1722,0.04438,-0.60844,41.4877,-89.5063,181.594,10,1,33.39,1019.48,-55.6646,0.0722656,0.320312,16.0391,3,3,3,3,0,-58.665,124.368,146.95,588.821,8.05342 +1949475,-0.24217,0.201605,1.9986,-4.7075,3.10625,-3.31625,0.1722,0.04438,-0.60844,41.4877,-89.5063,181.594,10,1,33.39,1019.48,-55.6646,0.0722656,0.320312,16.0391,3,3,3,3,0,-58.665,124.368,146.95,588.821,8.05342 +1949485,0.028792,-0.366,1.9986,-4.025,2.30125,-4.62,0.18228,0.03248,-0.60578,41.4877,-89.5063,181.594,10,1,33.39,1018.89,-50.4733,-0.0644531,0.394531,16.0645,3,3,3,3,0,-51.7161,132.203,147.491,677.858,8.05342 +1949495,0.028792,-0.366,1.9986,-4.025,2.30125,-4.62,0.18228,0.03248,-0.60578,41.4877,-89.5063,181.594,10,1,33.39,1018.89,-50.4733,-0.0644531,0.394531,16.0645,3,3,3,3,0,-51.7161,132.203,147.491,677.858,8.05342 +1949505,0.118218,0.030744,1.9986,-4.025,2.45875,-5.3725,0.1911,0.0217,-0.60508,41.4877,-89.5063,181.594,10,1,33.39,1017.97,-42.3726,-0.0644531,0.394531,16.0645,3,3,3,3,0,-51.7161,132.203,147.491,677.858,8.05342 +1949515,0.118218,0.030744,1.9986,-4.025,2.45875,-5.3725,0.1911,0.0217,-0.60508,41.4877,-89.5063,181.594,10,1,33.39,1017.97,-42.3726,0.0253906,0.359375,16.0938,3,3,3,3,0,-51.7161,132.203,147.491,677.858,8.06309 +1949525,0.118218,0.030744,1.9986,-4.025,2.45875,-5.3725,0.1911,0.0217,-0.60508,41.4877,-89.5063,181.594,10,1,33.39,1017.97,-42.3726,0.0253906,0.359375,16.0938,3,3,3,3,0,-51.7161,132.203,147.491,677.858,8.06309 +1949535,-0.108336,0.159637,1.9986,-3.64875,2.70375,-4.9525,0.18732,-0.00476,-0.59346,41.4877,-89.5063,181.594,10,1,33.39,1017.39,-37.262,-0.0703125,0.339844,16.1406,3,3,3,3,0,-44.2784,140.082,148.13,771.676,8.06309 +1949545,-0.108336,0.159637,1.9986,-3.64875,2.70375,-4.9525,0.18732,-0.00476,-0.59346,41.4877,-89.5063,181.594,10,1,33.39,1017.39,-37.262,-0.0703125,0.339844,16.1406,3,3,3,3,0,-44.2784,140.082,148.13,771.676,8.06309 +1949555,-0.108336,0.159637,1.9986,-3.64875,2.70375,-4.9525,0.18732,-0.00476,-0.59346,41.4877,-89.5063,181.594,10,1,33.39,1017.39,-37.262,-0.0703125,0.339844,16.1406,3,3,3,3,0,-44.2784,140.082,148.13,771.676,8.06309 +1949565,-0.023851,-0.18788,1.9986,-3.605,1.58375,-8.295,0.19754,-0.01876,-0.595,41.4877,-89.5063,181.594,10,1,33.39,1016.65,-30.7369,-0.162109,0.347656,16.1602,3,3,3,3,0,-44.2784,140.082,148.13,771.676,8.03408 +1949575,-0.023851,-0.18788,1.9986,-3.605,1.58375,-8.295,0.19754,-0.01876,-0.595,41.4877,-89.5063,181.594,10,1,33.39,1016.65,-30.7369,-0.162109,0.347656,16.1602,3,3,3,3,0,-44.2784,140.082,148.13,771.676,8.03408 +1949585,0.169214,0.111508,1.9986,-5.5125,0.91875,7.90125,0.20636,-0.03612,-0.5957,41.4877,-89.5063,181.77,10,1,33.39,1015.77,-22.9714,-0.128906,0.376953,16.1621,3,3,3,3,0,-36.4738,147.847,148.372,870.233,8.01475 +1949595,0.169214,0.111508,1.9986,-5.5125,0.91875,7.90125,0.20636,-0.03612,-0.5957,41.4877,-89.5063,181.77,10,1,33.39,1015.77,-22.9714,-0.128906,0.376953,16.1621,3,3,3,3,0,-36.4738,147.847,148.372,870.233,8.01475 +1949605,0.169214,0.111508,1.9986,-5.5125,0.91875,7.90125,0.20636,-0.03612,-0.5957,41.4877,-89.5063,181.77,10,1,33.39,1015.77,-22.9714,-0.128906,0.376953,16.1621,3,3,3,3,0,-36.4738,147.847,148.372,870.233,8.01475 +1949615,0.099796,-0.102358,1.9986,-4.9525,8.225,0.5075,0.21994,-0.04984,-0.60004,41.4877,-89.5063,181.77,10,1,33.4,1015.16,-17.5857,-0.185547,0.287109,16.1797,3,3,3,3,0,-36.4738,147.847,148.372,870.233,8.00508 +1949625,0.099796,-0.102358,1.9986,-4.9525,8.225,0.5075,0.21994,-0.04984,-0.60004,41.4877,-89.5063,181.77,10,1,33.4,1015.16,-17.5857,-0.185547,0.287109,16.1797,3,3,3,3,0,-36.4738,147.847,148.372,870.233,8.00508 +1949635,0.099796,-0.102358,1.9986,-4.9525,8.225,0.5075,0.21994,-0.04984,-0.60004,41.4877,-89.5063,181.77,10,1,33.4,1014.51,-11.8415,-0.185547,0.287109,16.1797,3,3,3,3,0,-36.4738,147.847,148.372,870.233,8.00508 +1949645,-0.013847,-0.052338,1.9986,-4.26125,5.4425,-1.40875,0.22918,-0.06594,-0.59738,41.4877,-89.5063,181.77,10,1,33.4,1014.51,-11.8415,-0.230469,0.291016,16.2012,3,3,3,3,0,-28.0953,155.776,148.638,971.145,8.03408 +1949655,-0.013847,-0.052338,1.9986,-4.26125,5.4425,-1.40875,0.22918,-0.06594,-0.59738,41.4877,-89.5063,181.77,10,1,33.4,1014.51,-11.8415,-0.230469,0.291016,16.2012,3,3,3,3,0,-28.0953,155.776,148.638,971.145,8.03408 +1949665,0.20008,-0.018788,1.9986,-6.41375,1.53125,9.86125,0.24822,-0.08946,-0.59388,41.4877,-89.5063,181.77,10,1,33.4,1014.09,-8.12748,-0.201172,0.341797,16.207,3,3,3,3,0,-28.0953,155.776,148.638,971.145,8.01475 +1949675,0.20008,-0.018788,1.9986,-6.41375,1.53125,9.86125,0.24822,-0.08946,-0.59388,41.4877,-89.5063,181.77,10,1,33.4,1014.09,-8.12748,-0.201172,0.341797,16.207,3,3,3,3,0,-28.0953,155.776,148.638,971.145,8.01475 +1949685,0.20008,-0.018788,1.9986,-6.41375,1.53125,9.86125,0.24822,-0.08946,-0.59388,41.4877,-89.5063,181.77,10,1,33.4,1014.09,-8.12748,-0.201172,0.341797,16.207,3,3,3,3,0,-28.0953,155.776,148.638,971.145,8.01475 +1949695,0.101016,-0.191845,1.9986,-4.64625,4.27875,-2.0475,0.28238,-0.10024,-0.59052,41.4877,-89.5063,181.77,10,1,33.4,1013.78,-5.38638,-0.222656,0.244141,16.1992,3,3,3,3,0,-17.3054,165.635,148.599,1077.79,8.03408 +1949705,0.101016,-0.191845,1.9986,-4.64625,4.27875,-2.0475,0.28238,-0.10024,-0.59052,41.4877,-89.5063,181.77,10,1,33.4,1013.78,-5.38638,-0.222656,0.244141,16.1992,3,3,3,3,0,-17.3054,165.635,148.599,1077.79,8.03408 +1949715,0.154086,0.256017,1.9986,-4.4975,1.855,-1.6275,0.32746,-0.11732,-0.6041,41.4877,-89.5063,181.77,10,1,33.38,1013.41,-2.11205,-0.222656,0.244141,16.1992,3,3,3,3,0,-17.3054,165.635,148.599,1077.79,8.03408 +1949725,0.154086,0.256017,1.9986,-4.4975,1.855,-1.6275,0.32746,-0.11732,-0.6041,41.4877,-89.5063,181.77,10,1,33.38,1013.41,-2.11205,-0.222656,0.244141,16.1992,3,3,3,3,0,-17.3054,165.635,148.599,1077.79,8.03408 +1949735,0.154086,0.256017,1.9986,-4.4975,1.855,-1.6275,0.32746,-0.11732,-0.6041,41.4877,-89.5063,181.77,10,1,33.38,1013.41,-2.11205,-0.21875,0.234375,16.1836,3,3,3,3,0,-17.3054,165.635,148.599,1077.79,8.03408 +1949745,0.154086,0.256017,1.9986,-4.4975,1.855,-1.6275,0.32746,-0.11732,-0.6041,41.4877,-89.5063,181.77,10,1,33.38,1013.41,-2.11205,-0.21875,0.234375,16.1836,3,3,3,3,0,-17.3054,165.635,148.599,1077.79,8.03408 +1949755,0.12383,0.17141,1.9986,-4.095,0.525,-4.6375,0.35896,-0.1162,-0.60382,41.4877,-89.5063,181.77,10,1,33.38,1013.08,0.808183,-0.25,0.207031,16.166,3,3,3,3,0,-8.0911,173.541,148.171,1214.89,8.03408 +1949765,0.12383,0.17141,1.9986,-4.095,0.525,-4.6375,0.35896,-0.1162,-0.60382,41.4877,-89.5063,181.77,10,1,33.38,1013.08,0.808183,-0.25,0.207031,16.166,3,3,3,3,0,-8.0911,173.541,148.171,1214.89,8.03408 +1949775,0.12383,0.17141,1.9986,-4.095,0.525,-4.6375,0.35896,-0.1162,-0.60382,41.4877,-89.5063,181.77,10,1,33.38,1013.08,0.808183,-0.25,0.207031,16.166,3,3,3,3,0,-8.0911,173.541,148.171,1214.89,8.03408 +1949785,0.10248,0.194712,1.9986,-3.6225,2.44125,-5.915,0.39536,-0.10878,-0.59262,41.4877,-89.5063,181.898,9,1,33.38,1012.53,5.67806,-0.304688,0.230469,16.1504,3,3,3,3,0,-8.0911,173.541,148.171,1214.89,8.07275 +1949795,0.10248,0.194712,1.9986,-3.6225,2.44125,-5.915,0.39536,-0.10878,-0.59262,41.4877,-89.5063,181.898,9,1,33.38,1012.53,5.67806,-0.304688,0.230469,16.1504,3,3,3,3,0,-8.0911,173.541,148.171,1214.89,8.07275 +1949805,0.10248,0.194712,1.9986,-3.6225,2.44125,-5.915,0.39536,-0.10878,-0.59262,41.4877,-89.5063,181.898,9,1,33.39,1011.72,12.8545,-0.304688,0.230469,16.1504,3,3,3,3,0,-8.0911,173.541,148.171,1214.89,8.07275 +1949815,0.121939,0.188063,1.9986,-3.75375,0.8925,-4.2175,0.42728,-0.08946,-0.58478,41.4877,-89.5063,181.898,9,1,33.39,1011.72,12.8545,-0.341797,0.207031,16.1445,3,3,3,3,0,1.66238,181.46,148.173,1328.54,8.03408 +1949825,0.121939,0.188063,1.9986,-3.75375,0.8925,-4.2175,0.42728,-0.08946,-0.58478,41.4877,-89.5063,181.898,9,1,33.39,1011.72,12.8545,-0.341797,0.207031,16.1445,3,3,3,3,0,1.66238,181.46,148.173,1328.54,8.03408 +1949835,0.0427,0.227957,1.9986,-2.45875,-3.0625,-6.0375,0.45724,-0.0546,-0.57778,41.4877,-89.5063,181.898,9,1,33.39,1010.65,22.344,-0.390625,0.171875,16.1562,3,3,3,3,0,1.66238,181.46,148.173,1328.54,8.03408 +1949845,0.0427,0.227957,1.9986,-2.45875,-3.0625,-6.0375,0.45724,-0.0546,-0.57778,41.4877,-89.5063,181.898,9,1,33.39,1010.65,22.344,-0.390625,0.171875,16.1562,3,3,3,3,0,1.66238,181.46,148.173,1328.54,8.03408 +1949855,0.0427,0.227957,1.9986,-2.45875,-3.0625,-6.0375,0.45724,-0.0546,-0.57778,41.4877,-89.5063,181.898,9,1,33.39,1010.65,22.344,-0.390625,0.171875,16.1562,3,3,3,3,0,1.66238,181.46,148.173,1328.54,8.03408 +1949865,0.103639,0.190991,1.9986,-2.68625,-2.835,-7.79625,0.47376,-0.007,-0.56686,41.4877,-89.5063,181.898,9,1,33.39,1009.22,35.0411,-0.443359,0.199219,16.166,3,3,3,3,0,12.1357,189.435,148.327,1445.38,8.04375 +1949875,0.103639,0.190991,1.9986,-2.68625,-2.835,-7.79625,0.47376,-0.007,-0.56686,41.4877,-89.5063,181.898,9,1,33.39,1009.22,35.0411,-0.443359,0.199219,16.166,3,3,3,3,0,12.1357,189.435,148.327,1445.38,8.04375 +1949885,0.188246,0.320128,1.9986,-0.2975,1.12,-24.29,0.48566,0.04942,-0.5642,41.4877,-89.5063,181.898,9,1,33.39,1007.53,50.0704,-0.470703,0.173828,16.1738,3,3,3,3,0,12.1357,189.435,148.327,1445.38,8.04375 +1949895,0.188246,0.320128,1.9986,-0.2975,1.12,-24.29,0.48566,0.04942,-0.5642,41.4877,-89.5063,181.898,9,1,33.39,1007.53,50.0704,-0.470703,0.173828,16.1738,3,3,3,3,0,12.1357,189.435,148.327,1445.38,8.04375 +1949905,0.188246,0.320128,1.9986,-0.2975,1.12,-24.29,0.48566,0.04942,-0.5642,41.4877,-89.5063,181.898,9,1,33.39,1007.53,50.0704,-0.470703,0.173828,16.1738,3,3,3,3,0,12.1357,189.435,148.327,1445.38,8.04375 +1949915,0.168726,0.620492,1.9986,-4.27,-1.58375,1.015,0.46088,0.13342,-0.55356,41.4877,-89.5063,181.898,9,1,33.39,1005.43,68.7809,-0.521484,0.123047,16.1777,3,3,3,3,0,23.545,197.493,148.142,1566.01,8.05342 +1949925,0.168726,0.620492,1.9986,-4.27,-1.58375,1.015,0.46088,0.13342,-0.55356,41.4877,-89.5063,181.898,9,1,33.39,1005.43,68.7809,-0.521484,0.123047,16.1777,3,3,3,3,0,23.545,197.493,148.142,1566.01,8.05342 +1949935,0.168726,0.620492,1.9986,-4.27,-1.58375,1.015,0.46088,0.13342,-0.55356,41.4877,-89.5063,181.898,9,1,33.39,1002.94,91.0169,-0.521484,0.123047,16.1777,3,3,3,3,0,23.545,197.493,148.142,1566.01,8.05342 +1949945,0.309941,0.543388,1.9986,-1.3125,-2.8875,-13.2475,0.42098,0.17444,-0.53102,41.4877,-89.5063,181.898,9,1,33.39,1002.94,91.0169,-0.730469,0.0488281,16.1504,3,3,3,3,0,23.545,197.493,148.142,1566.01,8.04375 +1949955,0.309941,0.543388,1.9986,-1.3125,-2.8875,-13.2475,0.42098,0.17444,-0.53102,41.4877,-89.5063,181.898,9,1,33.39,1002.94,91.0169,-0.730469,0.0488281,16.1504,3,3,3,3,0,23.545,197.493,148.142,1566.01,8.04375 +1949965,0.377468,0.920368,1.9986,-2.8875,2.10875,-8.6275,0.38024,0.20762,-0.5306,41.4877,-89.5063,181.898,9,1,33.39,1000.98,108.559,-0.791016,-0.0761719,16.1367,3,3,3,3,0,35.7152,205.597,148.054,1690.87,8.05342 +1949975,0.377468,0.920368,1.9986,-2.8875,2.10875,-8.6275,0.38024,0.20762,-0.5306,41.4877,-89.5063,181.898,9,1,33.39,1000.98,108.559,-0.791016,-0.0761719,16.1367,3,3,3,3,0,35.7152,205.597,148.054,1690.87,8.05342 +1949985,0.377468,0.920368,1.9986,-2.8875,2.10875,-8.6275,0.38024,0.20762,-0.5306,41.4877,-89.5063,181.898,9,1,33.39,1000.98,108.559,-0.791016,-0.0761719,16.1367,3,3,3,3,0,35.7152,205.597,148.054,1690.87,8.05342 +1949995,0.485804,0.90524,1.9986,-3.15,2.17875,-10.8588,0.33054,0.23464,-0.52934,41.4877,-89.5063,182.057,10,1,33.39,999.43,122.455,-0.875,-0.210938,16.1426,3,3,3,3,0,35.7152,205.597,148.054,1690.87,8.03408 +1950005,0.485804,0.90524,1.9986,-3.15,2.17875,-10.8588,0.33054,0.23464,-0.52934,41.4877,-89.5063,182.057,10,1,33.39,999.43,122.455,-0.875,-0.210938,16.1426,3,3,3,3,0,35.7152,205.597,148.054,1690.87,8.03408 +1950015,0.35014,0.891576,1.9986,-3.45625,2.0475,-6.08125,0.27566,0.2471,-0.52598,41.4877,-89.5063,182.057,10,1,33.4,997.83,136.828,-0.875,-0.210938,16.1426,3,3,3,3,0,35.7152,205.597,148.054,1690.87,8.03408 +1950025,0.35014,0.891576,1.9986,-3.45625,2.0475,-6.08125,0.27566,0.2471,-0.52598,41.4877,-89.5063,182.057,10,1,33.4,997.83,136.828,-1.18555,-0.398438,16.1895,3,3,3,3,0,49.3604,214.35,148.592,1819.1,8.02441 +1950035,0.35014,0.891576,1.9986,-3.45625,2.0475,-6.08125,0.27566,0.2471,-0.52598,41.4877,-89.5063,182.057,10,1,33.4,997.83,136.828,-1.18555,-0.398438,16.1895,3,3,3,3,0,49.3604,214.35,148.592,1819.1,8.02441 +1950045,0.393999,0.796111,1.9986,-5.95875,1.085,5.52125,0.21896,0.23716,-0.53088,41.4877,-89.5063,182.057,10,1,33.4,996.58,148.07,-1.22852,-0.40625,16.2051,3,3,3,3,0,49.3604,214.35,148.592,1819.1,8.01475 +1950055,0.393999,0.796111,1.9986,-5.95875,1.085,5.52125,0.21896,0.23716,-0.53088,41.4877,-89.5063,182.057,10,1,33.4,996.58,148.07,-1.22852,-0.40625,16.2051,3,3,3,3,0,49.3604,214.35,148.592,1819.1,8.01475 +1950065,0.393999,0.796111,1.9986,-5.95875,1.085,5.52125,0.21896,0.23716,-0.53088,41.4877,-89.5063,182.057,10,1,33.4,996.58,148.07,-1.22852,-0.40625,16.2051,3,3,3,3,0,49.3604,214.35,148.592,1819.1,8.01475 +1950075,0.521489,0.643428,1.9986,-2.8875,-0.28,-13.93,0.15988,0.20594,-0.53942,41.4877,-89.5063,182.057,10,1,33.4,996.58,148.07,-1.23633,-0.412109,16.2031,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.05342 +1950085,0.521489,0.643428,1.9986,-2.8875,-0.28,-13.93,0.15988,0.20594,-0.53942,41.4877,-89.5063,182.057,10,1,33.4,996.58,148.07,-1.23633,-0.412109,16.2031,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.05342 +1950095,0.521489,0.643428,1.9986,-2.8875,-0.28,-13.93,0.15988,0.20594,-0.53942,41.4877,-89.5063,182.057,10,1,33.4,998.13,134.132,-1.23633,-0.412109,16.2031,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.05342 +1950105,0.358558,0.898713,1.9986,-2.835,0.44625,-5.3725,0.11158,0.15442,-0.55216,41.4877,-89.5063,182.057,10,1,33.4,998.13,134.132,-1.21094,-0.46875,16.2207,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.03408 +1950115,0.358558,0.898713,1.9986,-2.835,0.44625,-5.3725,0.11158,0.15442,-0.55216,41.4877,-89.5063,182.057,10,1,33.4,998.13,134.132,-1.21094,-0.46875,16.2207,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.03408 +1950125,0.3843,0.428464,1.9986,-0.83125,-1.65375,-19.8013,0.08974,0.04592,-0.57162,41.4877,-89.5063,182.057,10,1,33.38,1000.6,111.961,-1.21094,-0.46875,16.2207,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.05342 +1950135,0.3843,0.428464,1.9986,-0.83125,-1.65375,-19.8013,0.08974,0.04592,-0.57162,41.4877,-89.5063,182.057,10,1,33.38,1000.6,111.961,-1.21094,-0.46875,16.2207,3,3,3,3,0,64.9578,224.407,148.788,1960.28,8.05342 +1950145,0.3843,0.428464,1.9986,-0.83125,-1.65375,-19.8013,0.08974,0.04592,-0.57162,41.4877,-89.5063,182.057,10,1,33.38,1000.6,111.961,-1.27148,-0.320312,16.1992,3,3,3,3,0,77.0048,232.059,148.314,2124.99,8.05342 +1950155,0.3843,0.428464,1.9986,-0.83125,-1.65375,-19.8013,0.08974,0.04592,-0.57162,41.4877,-89.5063,182.057,10,1,33.38,1000.6,111.961,-1.27148,-0.320312,16.1992,3,3,3,3,0,77.0048,232.059,148.314,2124.99,8.05342 +1950165,0.210999,0.18544,1.9986,-4.6375,4.64625,-4.90875,0.12362,-0.07728,-0.581,41.4877,-89.5063,182.057,10,1,33.38,1001.86,100.675,-1.20703,-0.222656,16.1641,3,3,3,3,0,77.0048,232.059,148.314,2124.99,8.06309 +1950175,0.210999,0.18544,1.9986,-4.6375,4.64625,-4.90875,0.12362,-0.07728,-0.581,41.4877,-89.5063,182.057,10,1,33.38,1001.86,100.675,-1.20703,-0.222656,16.1641,3,3,3,3,0,77.0048,232.059,148.314,2124.99,8.06309 +1950185,0.210999,0.18544,1.9986,-4.6375,4.64625,-4.90875,0.12362,-0.07728,-0.581,41.4877,-89.5063,182.057,10,1,33.38,1002.61,93.9652,-1.20703,-0.222656,16.1641,3,3,3,3,0,77.0048,232.059,148.314,2124.99,8.06309 +1950195,0.698145,-0.12871,1.9986,-1.40875,5.71375,-14.245,0.18032,-0.12978,-0.59248,41.4877,-89.5063,182.22,10,1,33.38,1002.61,93.9652,-0.898438,-0.212891,16.084,3,3,3,3,0,89.0117,239.593,147.462,2252.18,8.04375 +1950205,0.698145,-0.12871,1.9986,-1.40875,5.71375,-14.245,0.18032,-0.12978,-0.59248,41.4877,-89.5063,182.22,10,1,33.38,1002.61,93.9652,-0.898438,-0.212891,16.084,3,3,3,3,0,89.0117,239.593,147.462,2252.18,8.04375 +1950215,0.413702,-0.343979,1.9986,-4.83,1.68,-3.5,0.26334,-0.15876,-0.59192,41.4877,-89.5063,182.22,10,1,33.38,1003.27,88.0641,-0.742188,-0.324219,16.0723,3,3,3,3,0,89.0117,239.593,147.462,2252.18,8.05342 +1950225,0.413702,-0.343979,1.9986,-4.83,1.68,-3.5,0.26334,-0.15876,-0.59192,41.4877,-89.5063,182.22,10,1,33.38,1003.27,88.0641,-0.742188,-0.324219,16.0723,3,3,3,3,0,89.0117,239.593,147.462,2252.18,8.05342 +1950235,0.413702,-0.343979,1.9986,-4.83,1.68,-3.5,0.26334,-0.15876,-0.59192,41.4877,-89.5063,182.22,10,1,33.38,1003.27,88.0641,-0.742188,-0.324219,16.0723,3,3,3,3,0,89.0117,239.593,147.462,2252.18,8.05342 +1950245,0.669536,-0.231312,1.9986,-4.47125,-1.07625,-2.4675,0.33124,-0.14126,-0.59612,41.4877,-89.5063,182.22,10,1,33.39,1004.23,79.4902,-0.541016,-0.410156,16.0586,3,3,3,3,0,101.37,247.309,147.378,2379.14,8.04375 +1950255,0.669536,-0.231312,1.9986,-4.47125,-1.07625,-2.4675,0.33124,-0.14126,-0.59612,41.4877,-89.5063,182.22,10,1,33.39,1004.23,79.4902,-0.541016,-0.410156,16.0586,3,3,3,3,0,101.37,247.309,147.378,2379.14,8.04375 +1950265,0.744932,0.29768,1.9986,-3.75375,2.56375,-3.87625,0.38024,-0.09436,-0.59892,41.4877,-89.5063,182.22,10,1,33.39,1005.21,70.7433,-0.433594,-0.703125,16.0742,3,3,3,3,0,101.37,247.309,147.378,2379.14,8.03408 +1950275,0.744932,0.29768,1.9986,-3.75375,2.56375,-3.87625,0.38024,-0.09436,-0.59892,41.4877,-89.5063,182.22,10,1,33.39,1005.21,70.7433,-0.433594,-0.703125,16.0742,3,3,3,3,0,101.37,247.309,147.378,2379.14,8.03408 +1950285,0.744932,0.29768,1.9986,-3.75375,2.56375,-3.87625,0.38024,-0.09436,-0.59892,41.4877,-89.5063,182.22,10,1,33.39,1005.21,70.7433,-0.433594,-0.703125,16.0742,3,3,3,3,0,101.37,247.309,147.378,2379.14,8.03408 +1950295,0.526247,0.214171,1.9986,-3.5,2.54625,-5.18,0.41244,-0.03808,-0.5971,41.4877,-89.5063,182.22,10,1,33.39,1004.03,81.2764,-0.583984,-0.820312,16.084,3,3,3,3,0,114.505,255.088,147.301,2510.7,8.06309 +1950305,0.526247,0.214171,1.9986,-3.5,2.54625,-5.18,0.41244,-0.03808,-0.5971,41.4877,-89.5063,182.22,10,1,33.39,1004.03,81.2764,-0.583984,-0.820312,16.084,3,3,3,3,0,114.505,255.088,147.301,2510.7,8.06309 +1950315,0.526247,0.214171,1.9986,-3.5,2.54625,-5.18,0.41244,-0.03808,-0.5971,41.4877,-89.5063,182.22,10,1,33.39,1001.39,104.887,-0.583984,-0.820312,16.084,3,3,3,3,0,114.505,255.088,147.301,2510.7,8.06309 +1950325,0.538386,0.223748,1.9986,-3.85875,-3.05375,-14.6912,0.39704,0.03528,-0.60144,41.4877,-89.5063,182.22,10,1,33.39,1001.39,104.887,-0.748047,-0.630859,16.0664,3,3,3,3,0,114.505,255.088,147.301,2510.7,8.07275 +1950335,0.538386,0.223748,1.9986,-3.85875,-3.05375,-14.6912,0.39704,0.03528,-0.60144,41.4877,-89.5063,182.22,10,1,33.39,1001.39,104.887,-0.748047,-0.630859,16.0664,3,3,3,3,0,114.505,255.088,147.301,2510.7,8.07275 +1950345,0.283101,0.296094,1.9986,-3.35125,5.99375,-2.79125,0.35224,0.0826,-0.58968,41.4877,-89.5063,182.22,10,1,33.39,998.95,126.764,-0.775391,-0.517578,16.041,3,3,3,3,0,128.488,262.927,146.903,2645.35,8.03408 +1950355,0.283101,0.296094,1.9986,-3.35125,5.99375,-2.79125,0.35224,0.0826,-0.58968,41.4877,-89.5063,182.22,10,1,33.39,998.95,126.764,-0.775391,-0.517578,16.041,3,3,3,3,0,128.488,262.927,146.903,2645.35,8.03408 +1950365,0.283101,0.296094,1.9986,-3.35125,5.99375,-2.79125,0.35224,0.0826,-0.58968,41.4877,-89.5063,182.22,10,1,33.39,998.95,126.764,-0.775391,-0.517578,16.041,3,3,3,3,0,128.488,262.927,146.903,2645.35,8.03408 +1950375,-0.378017,-0.032025,1.9986,-4.795,-3.5875,-5.67,0.29848,0.11424,-0.58226,41.4877,-89.5063,182.22,10,1,33.4,997.05,143.841,-0.806641,-0.337891,16.0215,3,3,3,3,0,128.488,262.927,146.903,2645.35,8.03408 +1950385,-0.378017,-0.032025,1.9986,-4.795,-3.5875,-5.67,0.29848,0.11424,-0.58226,41.4877,-89.5063,182.22,10,1,33.4,997.05,143.841,-0.806641,-0.337891,16.0215,3,3,3,3,0,128.488,262.927,146.903,2645.35,8.03408 +1950395,-0.153354,-0.416569,1.9986,-6.92125,-4.445,13.79,0.23744,0.11592,-0.58828,41.4877,-89.5063,182.405,10,1,33.4,996.07,152.659,-0.576172,0.447266,15.9805,3,3,3,3,0,142.573,270.587,146.4,2783.29,8.03408 +1950405,-0.153354,-0.416569,1.9986,-6.92125,-4.445,13.79,0.23744,0.11592,-0.58828,41.4877,-89.5063,182.405,10,1,33.4,996.07,152.659,-0.576172,0.447266,15.9805,3,3,3,3,0,142.573,270.587,146.4,2783.29,8.03408 +1950415,-0.153354,-0.416569,1.9986,-6.92125,-4.445,13.79,0.23744,0.11592,-0.58828,41.4877,-89.5063,182.405,10,1,33.4,996.07,152.659,-0.576172,0.447266,15.9805,3,3,3,3,0,142.573,270.587,146.4,2783.29,8.03408 +1950425,0.000732,-0.3355,1.9986,-5.9325,8.79375,0.49875,0.18186,0.09268,-0.58464,41.4877,-89.5063,182.405,10,1,33.4,995.77,155.361,-0.322266,0.583984,15.9688,3,3,3,3,0,142.573,270.587,146.4,2783.29,8.03408 +1950435,0.000732,-0.3355,1.9986,-5.9325,8.79375,0.49875,0.18186,0.09268,-0.58464,41.4877,-89.5063,182.405,10,1,33.4,995.77,155.361,-0.322266,0.583984,15.9688,3,3,3,3,0,142.573,270.587,146.4,2783.29,8.03408 +1950445,0.000732,-0.3355,1.9986,-5.9325,8.79375,0.49875,0.18186,0.09268,-0.58464,41.4877,-89.5063,182.405,10,1,33.4,995.45,158.243,-0.322266,0.583984,15.9688,3,3,3,3,0,142.573,270.587,146.4,2783.29,8.03408 +1950455,0.064416,-0.076616,1.9986,-2.40625,0.30625,-24.3687,0.13776,0.01414,-0.58072,41.4877,-89.5063,182.405,10,1,33.4,995.45,158.243,-0.195312,0.525391,15.9746,3,3,3,3,0,157.148,278.362,146.698,2919.38,8.05342 +1950465,0.064416,-0.076616,1.9986,-2.40625,0.30625,-24.3687,0.13776,0.01414,-0.58072,41.4877,-89.5063,182.405,10,1,33.4,995.45,158.243,-0.195312,0.525391,15.9746,3,3,3,3,0,157.148,278.362,146.698,2919.38,8.05342 +1950475,-0.115351,-0.222528,1.9986,-5.45125,1.7325,3.92,0.133,-0.04004,-0.57316,41.4877,-89.5063,182.405,10,1,33.4,995.32,159.414,-0.222656,0.402344,16.0098,3,3,3,3,0,157.148,278.362,146.698,2919.38,8.03408 +1950485,-0.115351,-0.222528,1.9986,-5.45125,1.7325,3.92,0.133,-0.04004,-0.57316,41.4877,-89.5063,182.405,10,1,33.4,995.32,159.414,-0.222656,0.402344,16.0098,3,3,3,3,0,157.148,278.362,146.698,2919.38,8.03408 +1950495,-0.115351,-0.222528,1.9986,-5.45125,1.7325,3.92,0.133,-0.04004,-0.57316,41.4877,-89.5063,182.405,10,1,33.4,995.32,159.414,-0.222656,0.402344,16.0098,3,3,3,3,0,157.148,278.362,146.698,2919.38,8.03408 +1950505,0.075823,-0.206241,1.9986,-4.08625,5.31125,-4.61125,0.13706,-0.09548,-0.5705,41.4877,-89.5063,182.405,10,1,33.4,995.45,158.243,-0.0742188,0.525391,16.0098,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.02441 +1950515,0.075823,-0.206241,1.9986,-4.08625,5.31125,-4.61125,0.13706,-0.09548,-0.5705,41.4877,-89.5063,182.405,10,1,33.4,995.45,158.243,-0.0742188,0.525391,16.0098,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.02441 +1950525,0.261812,-0.319396,1.9986,-3.29875,1.46125,-7.4725,0.18928,-0.15596,-0.58422,41.4877,-89.5063,182.405,10,1,33.38,995.07,161.657,-0.0742188,0.525391,16.0098,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.02441 +1950535,0.261812,-0.319396,1.9986,-3.29875,1.46125,-7.4725,0.18928,-0.15596,-0.58422,41.4877,-89.5063,182.405,10,1,33.38,995.07,161.657,-0.0742188,0.525391,16.0098,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.06309 +1950545,0.261812,-0.319396,1.9986,-3.29875,1.46125,-7.4725,0.18928,-0.15596,-0.58422,41.4877,-89.5063,182.405,10,1,33.38,995.07,161.657,0,0.414062,16.0215,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.06309 +1950555,0.261812,-0.319396,1.9986,-3.29875,1.46125,-7.4725,0.18928,-0.15596,-0.58422,41.4877,-89.5063,182.405,10,1,33.38,995.07,161.657,0,0.414062,16.0215,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.06309 +1950565,-0.188307,-0.260531,1.9986,-3.84125,0.455,-5.81,0.24136,-0.1904,-0.57596,41.4877,-89.5063,182.405,10,1,33.39,994.93,162.923,0,0.414062,16.0215,3,3,3,3,0,175.797,288.144,146.731,3058.45,8.06309 +1950575,-0.188307,-0.260531,1.9986,-3.84125,0.455,-5.81,0.24136,-0.1904,-0.57596,41.4877,-89.5063,182.405,10,1,33.39,994.93,162.923,-0.119141,0.359375,16.0117,3,3,3,3,0,190.811,295.831,146.615,3233.93,8.07275 +1950585,-0.188307,-0.260531,1.9986,-3.84125,0.455,-5.81,0.24136,-0.1904,-0.57596,41.4877,-89.5063,182.405,10,1,33.39,994.93,162.923,-0.119141,0.359375,16.0117,3,3,3,3,0,190.811,295.831,146.615,3233.93,8.07275 +1950595,0.250466,-0.150792,1.9986,-3.2725,1.49625,-16.1,0.28854,-0.2044,-0.5768,41.4877,-89.5063,182.583,10,1,33.39,994.71,164.907,-0.119141,0.416016,15.9961,3,3,3,3,0,190.811,295.831,146.615,3233.93,8.04375 +1950605,0.250466,-0.150792,1.9986,-3.2725,1.49625,-16.1,0.28854,-0.2044,-0.5768,41.4877,-89.5063,182.583,10,1,33.39,994.71,164.907,-0.119141,0.416016,15.9961,3,3,3,3,0,190.811,295.831,146.615,3233.93,8.04375 +1950615,0.250466,-0.150792,1.9986,-3.2725,1.49625,-16.1,0.28854,-0.2044,-0.5768,41.4877,-89.5063,182.583,10,1,33.39,994.71,164.907,-0.119141,0.416016,15.9961,3,3,3,3,0,190.811,295.831,146.615,3233.93,8.04375 +1950625,-0.250588,-0.104127,1.9986,-3.6575,12.5475,-13.825,0.34454,-0.19992,-0.5712,41.4877,-89.5063,182.583,10,1,33.39,994.57,166.169,-0.0878906,0.388672,15.9707,3,3,3,3,0,205.981,303.453,146.075,3371.64,8.06309 +1950635,-0.250588,-0.104127,1.9986,-3.6575,12.5475,-13.825,0.34454,-0.19992,-0.5712,41.4877,-89.5063,182.583,10,1,33.39,994.57,166.169,-0.0878906,0.388672,15.9707,3,3,3,3,0,205.981,303.453,146.075,3371.64,8.06309 +1950645,0.122854,0.356362,1.9986,-1.84625,-2.5725,-12.565,0.39284,-0.17878,-0.56728,41.4877,-89.5063,182.583,10,1,33.39,994.61,165.809,-0.255859,0.292969,15.9355,3,3,3,3,0,205.981,303.453,146.075,3371.64,8.07275 +1950655,0.122854,0.356362,1.9986,-1.84625,-2.5725,-12.565,0.39284,-0.17878,-0.56728,41.4877,-89.5063,182.583,10,1,33.39,994.61,165.809,-0.255859,0.292969,15.9355,3,3,3,3,0,205.981,303.453,146.075,3371.64,8.07275 +1950665,0.122854,0.356362,1.9986,-1.84625,-2.5725,-12.565,0.39284,-0.17878,-0.56728,41.4877,-89.5063,182.583,10,1,33.39,994.61,165.809,-0.255859,0.292969,15.9355,3,3,3,3,0,205.981,303.453,146.075,3371.64,8.07275 +1950675,-0.305732,0.359778,1.9986,-1.39125,2.17,-21.945,0.43722,-0.14308,-0.56378,41.4877,-89.5063,182.583,10,1,33.39,994.54,166.44,-0.369141,0.365234,15.9062,3,3,3,3,0,221.291,310.997,145.402,3507.66,8.04375 +1950685,-0.305732,0.359778,1.9986,-1.39125,2.17,-21.945,0.43722,-0.14308,-0.56378,41.4877,-89.5063,182.583,10,1,33.39,994.54,166.44,-0.369141,0.365234,15.9062,3,3,3,3,0,221.291,310.997,145.402,3507.66,8.04375 +1950695,-0.305732,0.359778,1.9986,-1.39125,2.17,-21.945,0.43722,-0.14308,-0.56378,41.4877,-89.5063,182.583,10,1,33.39,994.58,166.079,-0.369141,0.365234,15.9062,3,3,3,3,0,221.291,310.997,145.402,3507.66,8.04375 +1950705,-0.036905,-0.352763,1.9986,-3.4825,5.76625,-0.93625,0.47236,-0.0679,-0.56476,41.4877,-89.5063,182.583,10,1,33.39,994.58,166.079,-0.433594,0.466797,15.8652,3,3,3,3,0,221.291,310.997,145.402,3507.66,8.03408 +1950715,-0.036905,-0.352763,1.9986,-3.4825,5.76625,-0.93625,0.47236,-0.0679,-0.56476,41.4877,-89.5063,182.583,10,1,33.39,994.58,166.079,-0.433594,0.466797,15.8652,3,3,3,3,0,221.291,310.997,145.402,3507.66,8.03408 +1950725,-0.122366,0.219173,1.9986,-3.2375,3.80625,-6.265,0.47404,-0.00714,-0.56532,41.4877,-89.5063,182.583,10,1,33.4,994.53,166.536,-0.353516,0.53125,15.7891,3,3,3,3,0,236.778,318.456,144.272,3641.08,8.05342 +1950735,-0.122366,0.219173,1.9986,-3.2375,3.80625,-6.265,0.47404,-0.00714,-0.56532,41.4877,-89.5063,182.583,10,1,33.4,994.53,166.536,-0.353516,0.53125,15.7891,3,3,3,3,0,236.778,318.456,144.272,3641.08,8.05342 +1950745,-0.122366,0.219173,1.9986,-3.2375,3.80625,-6.265,0.47404,-0.00714,-0.56532,41.4877,-89.5063,182.583,10,1,33.4,994.53,166.536,-0.353516,0.53125,15.7891,3,3,3,3,0,236.778,318.456,144.272,3641.08,8.05342 +1950755,-0.032269,0.146583,1.9986,-4.33125,2.45875,-0.91,0.45038,0.04718,-0.54824,41.4877,-89.5063,182.583,10,1,33.4,994.26,168.971,-0.335938,0.607422,15.7441,3,3,3,3,0,236.778,318.456,144.272,3641.08,8.05342 +1950765,-0.032269,0.146583,1.9986,-4.33125,2.45875,-0.91,0.45038,0.04718,-0.54824,41.4877,-89.5063,182.583,10,1,33.4,994.26,168.971,-0.335938,0.607422,15.7441,3,3,3,3,0,236.778,318.456,144.272,3641.08,8.05342 +1950775,-0.062708,0.041358,1.9986,-4.13,4.80375,-5.24125,0.42448,0.08806,-0.5502,41.4877,-89.5063,182.583,10,1,33.4,993.65,174.474,-0.332031,0.625,15.6895,3,3,3,3,0,252.559,325.826,142.616,3772.15,8.00508 +1950785,-0.062708,0.041358,1.9986,-4.13,4.80375,-5.24125,0.42448,0.08806,-0.5502,41.4877,-89.5063,182.583,10,1,33.4,993.65,174.474,-0.332031,0.625,15.6895,3,3,3,3,0,252.559,325.826,142.616,3772.15,8.00508 +1950795,-0.062708,0.041358,1.9986,-4.13,4.80375,-5.24125,0.42448,0.08806,-0.5502,41.4877,-89.5063,182.583,10,1,33.4,993.65,174.474,-0.332031,0.625,15.6895,3,3,3,3,0,252.559,325.826,142.616,3772.15,8.00508 +1950805,-0.005734,0.031537,1.9986,-3.59625,-4.445,-5.76625,0.39578,0.12446,-0.5621,41.4877,-89.5063,182.766,10,1,33.4,993.15,178.988,-0.195312,0.572266,15.5684,3,3,3,3,0,252.559,325.826,142.616,3772.15,8.03408 +1950815,-0.005734,0.031537,1.9986,-3.59625,-4.445,-5.76625,0.39578,0.12446,-0.5621,41.4877,-89.5063,182.766,10,1,33.4,993.15,178.988,-0.195312,0.572266,15.5684,3,3,3,3,0,252.559,325.826,142.616,3772.15,8.03408 +1950825,-0.005734,0.031537,1.9986,-3.59625,-4.445,-5.76625,0.39578,0.12446,-0.5621,41.4877,-89.5063,182.766,10,1,33.4,992.5,184.859,-0.195312,0.572266,15.5684,3,3,3,3,0,252.559,325.826,142.616,3772.15,8.03408 +1950835,0.124196,-0.358192,1.9986,-3.59625,3.1675,-5.5825,0.36554,0.14952,-0.56028,41.4877,-89.5063,182.766,10,1,33.4,992.5,184.859,-0.15625,0.519531,15.5,3,3,3,3,0,268.644,333.105,141.187,3900.55,8.00508 +1950845,0.124196,-0.358192,1.9986,-3.59625,3.1675,-5.5825,0.36554,0.14952,-0.56028,41.4877,-89.5063,182.766,10,1,33.4,992.5,184.859,-0.15625,0.519531,15.5,3,3,3,3,0,268.644,333.105,141.187,3900.55,8.00508 +1950855,-0.002379,-0.408761,1.9986,-4.34,5.08375,-4.445,0.32536,0.16534,-0.56238,41.4877,-89.5063,182.766,10,1,33.4,991.91,190.191,-0.0761719,0.425781,15.4258,3,3,3,3,0,268.644,333.105,141.187,3900.55,8.05342 +1950865,-0.002379,-0.408761,1.9986,-4.34,5.08375,-4.445,0.32536,0.16534,-0.56238,41.4877,-89.5063,182.766,10,1,33.4,991.91,190.191,-0.0761719,0.425781,15.4258,3,3,3,3,0,268.644,333.105,141.187,3900.55,8.05342 +1950875,-0.002379,-0.408761,1.9986,-4.34,5.08375,-4.445,0.32536,0.16534,-0.56238,41.4877,-89.5063,182.766,10,1,33.4,991.91,190.191,-0.0761719,0.425781,15.4258,3,3,3,3,0,268.644,333.105,141.187,3900.55,8.05342 +1950885,0.038857,-0.209474,1.9986,-4.99625,4.85625,5.25875,0.29372,0.17108,-0.56938,41.4877,-89.5063,182.766,10,1,33.4,991.28,195.889,-0.0292969,0.363281,15.248,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.00508 +1950895,0.038857,-0.209474,1.9986,-4.99625,4.85625,5.25875,0.29372,0.17108,-0.56938,41.4877,-89.5063,182.766,10,1,33.4,991.28,195.889,-0.0292969,0.363281,15.248,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.00508 +1950905,-0.001464,-0.315309,1.9986,-5.565,11.2,-3.57,0.26348,0.16436,-0.57428,41.4877,-89.5063,182.766,10,1,33.4,990.58,202.224,-0.0136719,0.220703,15.0684,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.02441 +1950915,-0.001464,-0.315309,1.9986,-5.565,11.2,-3.57,0.26348,0.16436,-0.57428,41.4877,-89.5063,182.766,10,1,33.4,990.58,202.224,-0.0136719,0.220703,15.0684,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.02441 +1950925,-0.001464,-0.315309,1.9986,-5.565,11.2,-3.57,0.26348,0.16436,-0.57428,41.4877,-89.5063,182.766,10,1,33.4,990.58,202.224,-0.0136719,0.220703,15.0684,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.02441 +1950935,0.118401,-0.052704,1.9986,-2.7475,14.3763,-11.2437,0.20244,0.12782,-0.59906,41.4877,-89.5063,182.766,10,1,33.38,989.53,211.721,-0.0136719,0.220703,15.0684,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.06309 +1950945,0.118401,-0.052704,1.9986,-2.7475,14.3763,-11.2437,0.20244,0.12782,-0.59906,41.4877,-89.5063,182.766,10,1,33.38,989.53,211.721,-0.0136719,0.220703,15.0684,3,3,3,3,0,290.984,342.65,137.827,4026.47,8.06309 +1950955,0.118401,-0.052704,1.9986,-2.7475,14.3763,-11.2437,0.20244,0.12782,-0.59906,41.4877,-89.5063,182.766,10,1,33.38,989.53,211.721,-0.00585938,0.228516,14.9648,3,3,3,3,0,307.032,349.232,134.473,4191.09,8.06309 +1950965,0.118401,-0.052704,1.9986,-2.7475,14.3763,-11.2437,0.20244,0.12782,-0.59906,41.4877,-89.5063,182.766,10,1,33.38,989.53,211.721,-0.00585938,0.228516,14.9648,3,3,3,3,0,307.032,349.232,134.473,4191.09,8.06309 +1950975,0.383568,0.010187,1.9986,-4.62875,-1.11125,-2.24,0.17458,0.09408,-0.6006,41.4877,-89.5063,182.766,10,1,33.39,989.05,216.079,-0.138672,0.0117188,14.7148,3,3,3,3,0,307.032,349.232,134.473,4191.09,8.06309 +1950985,0.383568,0.010187,1.9986,-4.62875,-1.11125,-2.24,0.17458,0.09408,-0.6006,41.4877,-89.5063,182.766,10,1,33.39,989.05,216.079,-0.138672,0.0117188,14.7148,3,3,3,3,0,307.032,349.232,134.473,4191.09,8.06309 +1950995,0.383568,0.010187,1.9986,-4.62875,-1.11125,-2.24,0.17458,0.09408,-0.6006,41.4877,-89.5063,182.766,10,1,33.39,989.05,216.079,-0.138672,0.0117188,14.7148,3,3,3,3,0,307.032,349.232,134.473,4191.09,8.06309 +1951005,0.02074,-0.022509,1.9986,-4.7075,-4.27875,-2.5375,0.15456,0.04872,-0.6034,41.4877,-89.5063,182.96,9,1,33.38,989.44,212.536,-0.195312,-0.09375,14.5723,3,3,3,3,0,323.083,355.668,131.605,4304.61,8.05342 +1951015,0.02074,-0.022509,1.9986,-4.7075,-4.27875,-2.5375,0.15456,0.04872,-0.6034,41.4877,-89.5063,182.96,9,1,33.38,989.44,212.536,-0.195312,-0.09375,14.5723,3,3,3,3,0,323.083,355.668,131.605,4304.61,8.05342 +1951025,0.256505,-0.186172,1.9986,-3.50875,4.0775,-7.7875,0.15064,-0.00896,-0.60312,41.4877,-89.5063,182.96,9,1,33.39,991.51,193.802,-0.246094,-0.0117188,14.4297,3,3,3,3,0,323.083,355.668,131.605,4304.61,8.04375 +1951035,0.256505,-0.186172,1.9986,-3.50875,4.0775,-7.7875,0.15064,-0.00896,-0.60312,41.4877,-89.5063,182.96,9,1,33.39,991.51,193.802,-0.246094,-0.0117188,14.4297,3,3,3,3,0,323.083,355.668,131.605,4304.61,8.04375 +1951045,0.256505,-0.186172,1.9986,-3.50875,4.0775,-7.7875,0.15064,-0.00896,-0.60312,41.4877,-89.5063,182.96,9,1,33.39,991.51,193.802,-0.246094,-0.0117188,14.4297,3,3,3,3,0,323.083,355.668,131.605,4304.61,8.04375 +1951055,0.006832,-0.393999,1.9986,-3.815,2.07375,-10.3075,0.17248,-0.06888,-0.59724,41.4877,-89.5063,182.96,9,1,33.39,988.75,218.799,-0.447266,0.0566406,14.1797,3,3,3,3,0,342.97,362.908,129.09,4415.8,8.06309 +1951065,0.006832,-0.393999,1.9986,-3.815,2.07375,-10.3075,0.17248,-0.06888,-0.59724,41.4877,-89.5063,182.96,9,1,33.39,988.75,218.799,-0.447266,0.0566406,14.1797,3,3,3,3,0,342.97,362.908,129.09,4415.8,8.06309 +1951075,0.006832,-0.393999,1.9986,-3.815,2.07375,-10.3075,0.17248,-0.06888,-0.59724,41.4877,-89.5063,182.96,9,1,33.39,976.46,330.963,-0.447266,0.0566406,14.1797,3,3,3,3,0,342.97,362.908,129.09,4415.8,8.06309 +1951085,0.139629,0.146156,1.9986,-2.59,-4.24375,-20.9212,0.20972,-0.1253,-0.59724,41.4877,-89.5063,182.96,9,1,33.39,976.46,330.963,-0.3125,0.0703125,14.0527,3,3,3,3,0,342.97,362.908,129.09,4415.8,8.07275 +1951095,0.139629,0.146156,1.9986,-2.59,-4.24375,-20.9212,0.20972,-0.1253,-0.59724,41.4877,-89.5063,182.96,9,1,33.39,976.46,330.963,-0.3125,0.0703125,14.0527,3,3,3,3,0,342.97,362.908,129.09,4415.8,8.07275 +1951105,0.549549,0.235399,1.9986,-2.2575,1.12,-3.42125,0.26306,-0.1596,-0.59402,41.4877,-89.5063,182.96,9,1,33.39,974.74,346.772,-0.3125,0.0703125,14.0527,3,3,3,3,0,342.97,362.908,129.09,4415.8,8.07275 +1951115,0.549549,0.235399,1.9986,-2.2575,1.12,-3.42125,0.26306,-0.1596,-0.59402,41.4877,-89.5063,182.96,9,1,33.39,974.74,346.772,-0.382812,0.0078125,13.8633,3,3,3,3,0,361.09,369.268,125.52,4543.2,8.07275 +1951125,0.549549,0.235399,1.9986,-2.2575,1.12,-3.42125,0.26306,-0.1596,-0.59402,41.4877,-89.5063,182.96,9,1,33.39,974.74,346.772,-0.382812,0.0078125,13.8633,3,3,3,3,0,361.09,369.268,125.52,4543.2,8.07275 +1951135,-0.133651,0.16958,1.9986,-5.69625,6.405,6.01125,0.32452,-0.17332,-0.5887,41.4877,-89.5063,182.96,9,1,33.39,976.56,330.044,-0.521484,-0.105469,13.7988,3,3,3,3,0,361.09,369.268,125.52,4543.2,8.03408 +1951145,-0.133651,0.16958,1.9986,-5.69625,6.405,6.01125,0.32452,-0.17332,-0.5887,41.4877,-89.5063,182.96,9,1,33.39,976.56,330.044,-0.521484,-0.105469,13.7988,3,3,3,3,0,361.09,369.268,125.52,4543.2,8.03408 +1951155,0.244732,-0.104188,1.9986,-2.98375,-9.345,-10.6663,0.37884,-0.16436,-0.56504,41.4877,-89.5063,182.96,9,1,33.39,978.44,312.797,-0.521484,-0.105469,13.7988,3,3,3,3,0,361.09,369.268,125.52,4543.2,8.03408 +1951165,0.244732,-0.104188,1.9986,-2.98375,-9.345,-10.6663,0.37884,-0.16436,-0.56504,41.4877,-89.5063,182.96,9,1,33.39,978.44,312.797,-0.585938,0.0410156,13.7324,3,3,3,3,0,379.638,375.624,123.349,4656.43,8.03408 +1951175,0.244732,-0.104188,1.9986,-2.98375,-9.345,-10.6663,0.37884,-0.16436,-0.56504,41.4877,-89.5063,182.96,9,1,33.39,978.44,312.797,-0.585938,0.0410156,13.7324,3,3,3,3,0,379.638,375.624,123.349,4656.43,8.03408 +1951185,0.229726,-0.074847,1.9986,-2.26625,-11.7863,-20.8075,0.4473,-0.08456,-0.55454,41.4877,-89.5063,182.96,9,1,33.39,978.45,312.706,-0.439453,0.0878906,13.5977,3,3,3,3,0,379.638,375.624,123.349,4656.43,8.02441 +1951195,0.229726,-0.074847,1.9986,-2.26625,-11.7863,-20.8075,0.4473,-0.08456,-0.55454,41.4877,-89.5063,182.96,9,1,33.39,978.45,312.706,-0.439453,0.0878906,13.5977,3,3,3,3,0,379.638,375.624,123.349,4656.43,8.02441 +1951205,0.229726,-0.074847,1.9986,-2.26625,-11.7863,-20.8075,0.4473,-0.08456,-0.55454,41.4877,-89.5063,182.96,9,1,33.39,976.88,327.106,-0.439453,0.0878906,13.5977,3,3,3,3,0,379.638,375.624,123.349,4656.43,8.02441 +1951215,0.073566,0.333792,1.9986,-2.91375,-1.3125,-16.905,0.46396,-0.01568,-0.55146,41.4878,-89.5062,183.147,8,1,33.39,976.88,327.106,-0.458984,0.0410156,13.5469,3,3,3,3,0,398.271,381.801,122.052,4771.09,8.01475 +1951225,0.073566,0.333792,1.9986,-2.91375,-1.3125,-16.905,0.46396,-0.01568,-0.55146,41.4878,-89.5062,183.147,8,1,33.39,976.88,327.106,-0.458984,0.0410156,13.5469,3,3,3,3,0,398.271,381.801,122.052,4771.09,8.01475 +1951235,-0.185318,-0.112789,1.9986,-5.55625,6.3175,2.52,0.44856,0.06622,-0.54768,41.4878,-89.5062,183.147,8,1,33.39,975.62,338.68,-0.560547,0.0917969,13.4766,3,3,3,3,0,398.271,381.801,122.052,4771.09,8.05342 +1951245,-0.185318,-0.112789,1.9986,-5.55625,6.3175,2.52,0.44856,0.06622,-0.54768,41.4878,-89.5062,183.147,8,1,33.39,975.62,338.68,-0.560547,0.0917969,13.4766,3,3,3,3,0,398.271,381.801,122.052,4771.09,8.05342 +1951255,-0.185318,-0.112789,1.9986,-5.55625,6.3175,2.52,0.44856,0.06622,-0.54768,41.4878,-89.5062,183.147,8,1,33.39,975.62,338.68,-0.560547,0.0917969,13.4766,3,3,3,3,0,398.271,381.801,122.052,4771.09,8.05342 +1951265,0.315309,-0.119865,1.9986,-4.19125,7.62125,-4.96125,0.4067,0.12992,-0.55524,41.4878,-89.5062,183.147,8,1,33.39,974.56,348.429,-0.474609,0.175781,13.3281,3,3,3,3,0,417.687,388.027,120.035,4884.77,8.04375 +1951275,0.315309,-0.119865,1.9986,-4.19125,7.62125,-4.96125,0.4067,0.12992,-0.55524,41.4878,-89.5062,183.147,8,1,33.39,974.56,348.429,-0.474609,0.175781,13.3281,3,3,3,3,0,417.687,388.027,120.035,4884.77,8.04375 +1951285,0.315309,-0.119865,1.9986,-4.19125,7.62125,-4.96125,0.4067,0.12992,-0.55524,41.4878,-89.5062,183.147,8,1,33.39,973.03,362.518,-0.474609,0.175781,13.3281,3,3,3,3,0,417.687,388.027,120.035,4884.77,8.04375 +1951295,0.025986,0.02318,1.9986,-4.08625,1.60125,-5.4425,0.34552,0.17528,-0.5523,41.4878,-89.5062,183.147,8,1,33.39,973.03,362.518,-0.513672,0.0703125,13.2617,3,3,3,3,0,417.687,388.027,120.035,4884.77,8.05342 +1951305,0.025986,0.02318,1.9986,-4.08625,1.60125,-5.4425,0.34552,0.17528,-0.5523,41.4878,-89.5062,183.147,8,1,33.39,973.03,362.518,-0.513672,0.0703125,13.2617,3,3,3,3,0,417.687,388.027,120.035,4884.77,8.05342 +1951315,0.322568,-0.102907,1.9986,-3.70125,-2.5375,-4.4975,0.27062,0.18928,-0.5628,41.4878,-89.5062,183.147,8,1,33.39,971.54,376.26,-0.658203,0.167969,13.1465,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.03408 +1951325,0.322568,-0.102907,1.9986,-3.70125,-2.5375,-4.4975,0.27062,0.18928,-0.5628,41.4878,-89.5062,183.147,8,1,33.39,971.54,376.26,-0.658203,0.167969,13.1465,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.03408 +1951335,0.322568,-0.102907,1.9986,-3.70125,-2.5375,-4.4975,0.27062,0.18928,-0.5628,41.4878,-89.5062,183.147,8,1,33.39,971.54,376.26,-0.658203,0.167969,13.1465,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.03408 +1951345,0.318176,0.308172,1.9986,-2.44125,4.03375,-12.3638,0.18858,0.14252,-0.58506,41.4878,-89.5062,183.147,8,1,33.37,968.92,400.45,-0.658203,0.167969,13.1465,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.06309 +1951355,0.318176,0.308172,1.9986,-2.44125,4.03375,-12.3638,0.18858,0.14252,-0.58506,41.4878,-89.5062,183.147,8,1,33.37,968.92,400.45,-0.658203,0.167969,13.1465,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.06309 +1951365,0.318176,0.308172,1.9986,-2.44125,4.03375,-12.3638,0.18858,0.14252,-0.58506,41.4878,-89.5062,183.147,8,1,33.37,968.92,400.45,-0.611328,0.167969,13.084,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.06309 +1951375,0.318176,0.308172,1.9986,-2.44125,4.03375,-12.3638,0.18858,0.14252,-0.58506,41.4878,-89.5062,183.147,8,1,33.37,968.92,400.45,-0.611328,0.167969,13.084,3,3,3,3,0,443.124,395.859,118.776,5001.72,8.06309 +1951385,0.214842,0.18971,1.9986,-4.64625,-2.75625,4.57625,0.16072,0.07322,-0.59864,41.4878,-89.5062,183.147,8,1,33.37,967.13,417.031,-0.763672,0.0136719,12.9785,3,3,3,3,0,463.713,401.977,116.86,5152.9,8.06309 +1951395,0.214842,0.18971,1.9986,-4.64625,-2.75625,4.57625,0.16072,0.07322,-0.59864,41.4878,-89.5062,183.147,8,1,33.37,967.13,417.031,-0.763672,0.0136719,12.9785,3,3,3,3,0,463.713,401.977,116.86,5152.9,8.06309 +1951405,0.273036,-0.040626,1.9986,-4.57625,-8.75,2.23125,0.16856,-0.04396,-0.60956,41.4878,-89.5062,183.338,9,1,33.37,965.4,433.086,-0.763672,0.0136719,12.9785,3,3,3,3,0,463.713,401.977,116.86,5152.9,8.08242 +1951415,0.273036,-0.040626,1.9986,-4.57625,-8.75,2.23125,0.16856,-0.04396,-0.60956,41.4878,-89.5062,183.338,9,1,33.37,965.4,433.086,-0.826172,-0.0449219,12.9395,3,3,3,3,0,463.713,401.977,116.86,5152.9,8.08242 +1951425,0.273036,-0.040626,1.9986,-4.57625,-8.75,2.23125,0.16856,-0.04396,-0.60956,41.4878,-89.5062,183.338,9,1,33.37,965.4,433.086,-0.826172,-0.0449219,12.9395,3,3,3,3,0,463.713,401.977,116.86,5152.9,8.08242 +1951435,0.085888,0.291336,1.9986,-0.665,-2.625,-20.09,0.21952,-0.10836,-0.6111,41.4878,-89.5062,183.338,9,1,33.37,964.21,444.146,-0.865234,-0.103516,12.8633,3,3,3,3,0,484.622,408.024,115.777,5274.9,8.07275 +1951445,0.085888,0.291336,1.9986,-0.665,-2.625,-20.09,0.21952,-0.10836,-0.6111,41.4878,-89.5062,183.338,9,1,33.37,964.21,444.146,-0.865234,-0.103516,12.8633,3,3,3,3,0,484.622,408.024,115.777,5274.9,8.07275 +1951455,0.085888,0.291336,1.9986,-0.665,-2.625,-20.09,0.21952,-0.10836,-0.6111,41.4878,-89.5062,183.338,9,1,33.38,963.16,453.931,-0.865234,-0.103516,12.8633,3,3,3,3,0,484.622,408.024,115.777,5274.9,8.07275 +1951465,0.19215,0.611098,1.9986,-2.38875,5.36375,-9.82625,0.29694,-0.14014,-0.60242,41.4878,-89.5062,183.338,9,1,33.38,963.16,453.931,-0.9375,-0.21875,12.8398,3,3,3,3,0,484.622,408.024,115.777,5274.9,8.05342 +1951475,0.19215,0.611098,1.9986,-2.38875,5.36375,-9.82625,0.29694,-0.14014,-0.60242,41.4878,-89.5062,183.338,9,1,33.38,963.16,453.931,-0.9375,-0.21875,12.8398,3,3,3,3,0,484.622,408.024,115.777,5274.9,8.05342 +1951485,0.257786,0.366976,1.9986,-5.29375,6.3175,0.385,0.36904,-0.13048,-0.58744,41.4878,-89.5062,183.338,9,1,33.38,961.75,467.068,-1.11523,-0.164062,12.8203,3,3,3,3,0,506.021,414.073,115.382,5398.84,8.05342 +1951495,0.257786,0.366976,1.9986,-5.29375,6.3175,0.385,0.36904,-0.13048,-0.58744,41.4878,-89.5062,183.338,9,1,33.38,961.75,467.068,-1.11523,-0.164062,12.8203,3,3,3,3,0,506.021,414.073,115.382,5398.84,8.05342 +1951505,0.257786,0.366976,1.9986,-5.29375,6.3175,0.385,0.36904,-0.13048,-0.58744,41.4878,-89.5062,183.338,9,1,33.38,961.75,467.068,-1.11523,-0.164062,12.8203,3,3,3,3,0,506.021,414.073,115.382,5398.84,8.05342 +1951515,0.196969,0.5551,1.9986,-4.15625,1.505,-4.75125,0.4263,-0.07966,-0.5747,41.4878,-89.5062,183.338,9,1,33.38,959.84,484.894,-1.10547,-0.00976562,12.8086,3,3,3,3,0,506.021,414.073,115.382,5398.84,8.07275 +1951525,0.196969,0.5551,1.9986,-4.15625,1.505,-4.75125,0.4263,-0.07966,-0.5747,41.4878,-89.5062,183.338,9,1,33.38,959.84,484.894,-1.10547,-0.00976562,12.8086,3,3,3,3,0,506.021,414.073,115.382,5398.84,8.07275 +1951535,0.106933,0.58804,1.9986,-2.93125,5.4775,-5.67,0.4585,-0.00028,-0.58198,41.4878,-89.5062,183.338,9,1,33.38,958.15,500.697,-1.10547,-0.00976562,12.8086,3,3,3,3,0,506.021,414.073,115.382,5398.84,8.07275 +1951545,0.106933,0.58804,1.9986,-2.93125,5.4775,-5.67,0.4585,-0.00028,-0.58198,41.4878,-89.5062,183.338,9,1,33.38,958.15,500.697,-1.14844,-0.0683594,12.8164,3,3,3,3,0,527.939,420.159,115.524,5526.5,8.04375 +1951555,0.106933,0.58804,1.9986,-2.93125,5.4775,-5.67,0.4585,-0.00028,-0.58198,41.4878,-89.5062,183.338,9,1,33.38,958.15,500.697,-1.14844,-0.0683594,12.8164,3,3,3,3,0,527.939,420.159,115.524,5526.5,8.04375 +1951565,-0.173362,0.712724,1.9986,-3.73625,2.5025,-5.4775,0.43582,0.0791,-0.55566,41.4878,-89.5062,183.338,9,1,33.38,956.32,517.84,-1.2168,-0.132812,12.8301,3,3,3,3,0,527.939,420.159,115.524,5526.5,8.04375 +1951575,-0.173362,0.712724,1.9986,-3.73625,2.5025,-5.4775,0.43582,0.0791,-0.55566,41.4878,-89.5062,183.338,9,1,33.38,956.32,517.84,-1.2168,-0.132812,12.8301,3,3,3,3,0,527.939,420.159,115.524,5526.5,8.04375 +1951585,-0.173362,0.712724,1.9986,-3.73625,2.5025,-5.4775,0.43582,0.0791,-0.55566,41.4878,-89.5062,183.338,9,1,33.38,955.04,529.85,-1.2168,-0.132812,12.8301,3,3,3,3,0,527.939,420.159,115.524,5526.5,8.04375 +1951595,0.2928,0.27999,1.9986,-3.52625,0.55125,4.96125,0.40012,0.1442,-0.55552,41.4878,-89.5062,183.338,9,1,33.38,955.04,529.85,-1.19922,0.228516,12.291,3,3,3,3,0,550.185,425.839,103.147,5658.93,8.05342 +1951605,0.2928,0.27999,1.9986,-3.52625,0.55125,4.96125,0.40012,0.1442,-0.55552,41.4878,-89.5062,183.338,9,1,33.38,955.04,529.85,-1.19922,0.228516,12.291,3,3,3,3,0,550.185,425.839,103.147,5658.93,8.05342 +1951615,0.169458,0.856928,1.9986,-6.09,2.93125,-2.07375,0.33866,0.1848,-0.5593,41.4878,-89.5062,183.503,8,1,33.38,953.85,541.03,-1.04688,0.113281,11.4043,3,3,3,3,0,550.185,425.839,103.147,5658.93,8.05342 +1951625,0.169458,0.856928,1.9986,-6.09,2.93125,-2.07375,0.33866,0.1848,-0.5593,41.4878,-89.5062,183.503,8,1,33.38,953.85,541.03,-1.04688,0.113281,11.4043,3,3,3,3,0,550.185,425.839,103.147,5658.93,8.05342 +1951635,0.169458,0.856928,1.9986,-6.09,2.93125,-2.07375,0.33866,0.1848,-0.5593,41.4878,-89.5062,183.503,8,1,33.38,953.85,541.03,-1.04688,0.113281,11.4043,3,3,3,3,0,550.185,425.839,103.147,5658.93,8.05342 +1951645,-0.16409,0.68747,1.9986,-4.9,6.545,3.22,0.26894,0.1981,-0.55398,41.4878,-89.5062,183.503,8,1,33.38,952.43,554.39,-1.05469,0.0292969,10.2188,3,3,3,3,0,572.83,430.282,72.605,5788.27,8.02441 +1951655,-0.16409,0.68747,1.9986,-4.9,6.545,3.22,0.26894,0.1981,-0.55398,41.4878,-89.5062,183.503,8,1,33.38,952.43,554.39,-1.05469,0.0292969,10.2188,3,3,3,3,0,572.83,430.282,72.605,5788.27,8.02441 +1951665,-0.16409,0.68747,1.9986,-4.9,6.545,3.22,0.26894,0.1981,-0.55398,41.4878,-89.5062,183.503,8,1,33.38,950.68,570.881,-1.05469,0.0292969,10.2188,3,3,3,3,0,572.83,430.282,72.605,5788.27,8.02441 +1951675,0.17202,0.650199,1.9986,-1.435,-3.71875,-18.165,0.20846,0.17752,-0.56322,41.4878,-89.5062,183.503,8,1,33.38,950.68,570.881,-1.11328,0.0996094,8.05273,3,3,3,3,0,572.83,430.282,72.605,5788.27,8.03408 +1951685,0.17202,0.650199,1.9986,-1.435,-3.71875,-18.165,0.20846,0.17752,-0.56322,41.4878,-89.5062,183.503,8,1,33.38,950.68,570.881,-1.11328,0.0996094,8.05273,3,3,3,3,0,572.83,430.282,72.605,5788.27,8.03408 +1951695,0.177022,0.878034,1.9986,-1.88125,4.62,-21.735,0.14896,0.10696,-0.57134,41.4878,-89.5062,183.503,8,1,33.38,948.97,587.026,-1.07031,0.0585938,7.36914,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.04375 +1951705,0.177022,0.878034,1.9986,-1.88125,4.62,-21.735,0.14896,0.10696,-0.57134,41.4878,-89.5062,183.503,8,1,33.38,948.97,587.026,-1.07031,0.0585938,7.36914,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.04375 +1951715,0.177022,0.878034,1.9986,-1.88125,4.62,-21.735,0.14896,0.10696,-0.57134,41.4878,-89.5062,183.503,8,1,33.38,948.97,587.026,-1.07031,0.0585938,7.36914,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.04375 +1951725,-0.156221,0.707417,1.9986,-2.49375,-3.17625,-6.16,0.1309,0.04536,-0.57974,41.4878,-89.5062,183.503,8,1,33.38,947.81,597.993,-1.20312,0.0488281,6.7168,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.03408 +1951735,-0.156221,0.707417,1.9986,-2.49375,-3.17625,-6.16,0.1309,0.04536,-0.57974,41.4878,-89.5062,183.503,8,1,33.38,947.81,597.993,-1.20312,0.0488281,6.7168,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.03408 +1951745,0.158356,1.04493,1.9986,-4.55875,-9.205,-5.1975,0.17136,-0.0462,-0.60256,41.4878,-89.5062,183.503,8,1,33.36,945.85,616.516,-1.20312,0.0488281,6.7168,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.03408 +1951755,0.158356,1.04493,1.9986,-4.55875,-9.205,-5.1975,0.17136,-0.0462,-0.60256,41.4878,-89.5062,183.503,8,1,33.36,945.85,616.516,-1.20312,0.0488281,6.7168,3,3,3,3,0,603.393,434.771,57.1038,5898.58,8.03408 +1951765,0.158356,1.04493,1.9986,-4.55875,-9.205,-5.1975,0.17136,-0.0462,-0.60256,41.4878,-89.5062,183.503,8,1,33.36,945.85,616.516,-1.1543,-0.0351562,6.54883,3,3,3,3,0,625.656,437.59,54.1949,6022.27,8.03408 +1951775,0.158356,1.04493,1.9986,-4.55875,-9.205,-5.1975,0.17136,-0.0462,-0.60256,41.4878,-89.5062,183.503,8,1,33.36,945.85,616.516,-1.1543,-0.0351562,6.54883,3,3,3,3,0,625.656,437.59,54.1949,6022.27,8.03408 +1951785,-0.022265,0.960628,1.9986,-3.78,0.30625,-6.50125,0.2065,-0.09828,-0.60424,41.4878,-89.5062,183.503,8,1,33.36,944.77,626.76,-1.1543,-0.0351562,6.54883,3,3,3,3,0,625.656,437.59,54.1949,6022.27,8.03408 +1951795,-0.022265,0.960628,1.9986,-3.78,0.30625,-6.50125,0.2065,-0.09828,-0.60424,41.4878,-89.5062,183.503,8,1,33.36,944.77,626.76,-1.23438,-0.0546875,6.53711,3,3,3,3,0,625.656,437.59,54.1949,6022.27,8.03408 +1951805,-0.022265,0.960628,1.9986,-3.78,0.30625,-6.50125,0.2065,-0.09828,-0.60424,41.4878,-89.5062,183.503,8,1,33.36,944.77,626.76,-1.23438,-0.0546875,6.53711,3,3,3,3,0,625.656,437.59,54.1949,6022.27,8.03408 +1951815,0.197945,0.72712,1.9986,-3.85,-3.21125,-2.5025,0.2709,-0.1295,-0.5999,41.4878,-89.5062,183.667,8,1,33.36,943.69,637.016,-1.25977,-0.015625,6.54102,3,3,3,3,0,648.412,440.378,53.7349,6105.46,8.06309 +1951825,0.197945,0.72712,1.9986,-3.85,-3.21125,-2.5025,0.2709,-0.1295,-0.5999,41.4878,-89.5062,183.667,8,1,33.36,943.69,637.016,-1.25977,-0.015625,6.54102,3,3,3,3,0,648.412,440.378,53.7349,6105.46,8.06309 +1951835,0.197945,0.72712,1.9986,-3.85,-3.21125,-2.5025,0.2709,-0.1295,-0.5999,41.4878,-89.5062,183.667,8,1,33.36,942.89,644.621,-1.25977,-0.015625,6.54102,3,3,3,3,0,648.412,440.378,53.7349,6105.46,8.06309 +1951845,0.009943,1.28149,1.9986,-2.86125,1.0325,-1.0325,0.329,-0.13454,-0.59346,41.4878,-89.5062,183.667,8,1,33.36,942.89,644.621,-1.22656,-0.0996094,6.52344,3,3,3,3,0,648.412,440.378,53.7349,6105.46,8.05342 +1951855,0.009943,1.28149,1.9986,-2.86125,1.0325,-1.0325,0.329,-0.13454,-0.59346,41.4878,-89.5062,183.667,8,1,33.36,942.89,644.621,-1.22656,-0.0996094,6.52344,3,3,3,3,0,648.412,440.378,53.7349,6105.46,8.05342 +1951865,0.161467,0.648247,1.9986,-4.865,-2.79125,4.305,0.38808,-0.12026,-0.59346,41.4878,-89.5062,183.667,8,1,33.37,942.17,651.491,-1.29492,-0.0683594,6.39258,3,3,3,3,0,671.611,443.052,49.7937,6190.01,8.06309 +1951875,0.161467,0.648247,1.9986,-4.865,-2.79125,4.305,0.38808,-0.12026,-0.59346,41.4878,-89.5062,183.667,8,1,33.37,942.17,651.491,-1.29492,-0.0683594,6.39258,3,3,3,3,0,671.611,443.052,49.7937,6190.01,8.06309 +1951885,0.161467,0.648247,1.9986,-4.865,-2.79125,4.305,0.38808,-0.12026,-0.59346,41.4878,-89.5062,183.667,8,1,33.37,942.17,651.491,-1.29492,-0.0683594,6.39258,3,3,3,3,0,671.611,443.052,49.7937,6190.01,8.06309 +1951895,0.240096,1.20396,1.9986,-0.455,-7.84,-19.6437,0.4417,-0.06804,-0.58044,41.4878,-89.5062,183.667,8,1,33.37,941.26,660.156,-1.24414,-0.0332031,6.07617,3,3,3,3,0,671.611,443.052,49.7937,6190.01,8.08242 +1951905,0.240096,1.20396,1.9986,-0.455,-7.84,-19.6437,0.4417,-0.06804,-0.58044,41.4878,-89.5062,183.667,8,1,33.37,941.26,660.156,-1.24414,-0.0332031,6.07617,3,3,3,3,0,671.611,443.052,49.7937,6190.01,8.08242 +1951915,0.058926,0.895114,1.9986,-6.125,7.9975,8.56625,0.4564,0.03528,-0.57848,41.4878,-89.5062,183.667,8,1,33.37,940.28,669.497,-1.24414,-0.0332031,6.07617,3,3,3,3,0,671.611,443.052,49.7937,6190.01,8.08242 +1951925,0.058926,0.895114,1.9986,-6.125,7.9975,8.56625,0.4564,0.03528,-0.57848,41.4878,-89.5062,183.667,8,1,33.37,940.28,669.497,-1.29297,-0.0722656,5.00781,3,3,3,3,0,694.87,445.107,33.9293,6274.25,8.06309 +1951935,0.058926,0.895114,1.9986,-6.125,7.9975,8.56625,0.4564,0.03528,-0.57848,41.4878,-89.5062,183.667,8,1,33.37,940.28,669.497,-1.29297,-0.0722656,5.00781,3,3,3,3,0,694.87,445.107,33.9293,6274.25,8.06309 +1951945,0.16714,0.938485,1.9986,-4.41875,7.3325,-8.8725,0.4389,0.10472,-0.56952,41.4878,-89.5062,183.667,8,1,33.37,939.2,679.803,-1.30469,-0.0351562,4.30273,3,3,3,3,0,694.87,445.107,33.9293,6274.25,8.05342 +1951955,0.16714,0.938485,1.9986,-4.41875,7.3325,-8.8725,0.4389,0.10472,-0.56952,41.4878,-89.5062,183.667,8,1,33.37,939.2,679.803,-1.30469,-0.0351562,4.30273,3,3,3,3,0,694.87,445.107,33.9293,6274.25,8.05342 +1951965,0.16714,0.938485,1.9986,-4.41875,7.3325,-8.8725,0.4389,0.10472,-0.56952,41.4878,-89.5062,183.667,8,1,33.37,938.12,690.12,-1.30469,-0.0351562,4.30273,3,3,3,3,0,694.87,445.107,33.9293,6274.25,8.05342 +1951975,0.104798,0.9699,1.82292,-5.55625,-0.1925,7.595,0.38262,0.14938,-0.55412,41.4878,-89.5062,183.667,8,1,33.37,938.12,690.12,-1.30469,-0.0546875,2.96875,3,3,3,3,0,718.14,446.23,15.7048,6346.86,8.06309 +1951985,0.104798,0.9699,1.82292,-5.55625,-0.1925,7.595,0.38262,0.14938,-0.55412,41.4878,-89.5062,183.667,8,1,33.37,938.12,690.12,-1.30469,-0.0546875,2.96875,3,3,3,3,0,718.14,446.23,15.7048,6346.86,8.06309 +1951995,0.053253,0.895724,1.3592,-3.78875,-12.25,-7.34125,0.32074,0.18718,-0.5565,41.4878,-89.5062,183.667,8,1,33.37,937.19,699.013,-1.33984,-0.0566406,2.41406,3,3,3,3,0,718.14,446.23,15.7048,6346.86,8.01475 +1952005,0.053253,0.895724,1.3592,-3.78875,-12.25,-7.34125,0.32074,0.18718,-0.5565,41.4878,-89.5062,183.667,8,1,33.37,937.19,699.013,-1.33984,-0.0566406,2.41406,3,3,3,3,0,718.14,446.23,15.7048,6346.86,8.01475 +1952015,0.053253,0.895724,1.3592,-3.78875,-12.25,-7.34125,0.32074,0.18718,-0.5565,41.4878,-89.5062,183.667,8,1,33.37,937.19,699.013,-1.33984,-0.0566406,2.41406,3,3,3,3,0,718.14,446.23,15.7048,6346.86,8.01475 +1952025,0.266265,1.02407,0.71736,-5.04875,-0.2975,6.20375,0.26166,0.18718,-0.55902,41.4878,-89.5062,183.826,8,1,33.37,936.27,707.821,-1.32031,-0.0410156,1.93359,3,3,3,3,0,742.226,446.787,9.42843,6400.32,8.05342 +1952035,0.266265,1.02407,0.71736,-5.04875,-0.2975,6.20375,0.26166,0.18718,-0.55902,41.4878,-89.5062,183.826,8,1,33.37,936.27,707.821,-1.32031,-0.0410156,1.93359,3,3,3,3,0,742.226,446.787,9.42843,6400.32,8.05342 +1952045,0.266265,1.02407,0.71736,-5.04875,-0.2975,6.20375,0.26166,0.18718,-0.55902,41.4878,-89.5062,183.826,8,1,33.37,935.37,716.444,-1.32031,-0.0410156,1.93359,3,3,3,3,0,742.226,446.787,9.42843,6400.32,8.05342 +1952055,-0.102297,1.13899,0.569496,-2.695,5.67875,-22.1025,0.19614,0.16478,-0.56966,41.4878,-89.5062,183.826,8,1,33.37,935.37,716.444,-1.36523,-0.0957031,1.16406,3,3,3,3,0,742.226,446.787,9.42843,6400.32,8.01475 +1952065,-0.102297,1.13899,0.569496,-2.695,5.67875,-22.1025,0.19614,0.16478,-0.56966,41.4878,-89.5062,183.826,8,1,33.37,935.37,716.444,-1.36523,-0.0957031,1.16406,3,3,3,3,0,742.226,446.787,9.42843,6400.32,8.01475 +1952075,0.205875,0.885293,0.326777,-2.51125,-2.63375,-18.8388,0.15442,0.11046,-0.57176,41.4878,-89.5062,183.826,8,1,33.37,934.27,726.996,-1.36523,-0.0957031,1.16406,3,3,3,3,0,742.226,446.787,9.42843,6400.32,8.01475 +1952085,0.205875,0.885293,0.326777,-2.51125,-2.63375,-18.8388,0.15442,0.11046,-0.57176,41.4878,-89.5062,183.826,8,1,33.37,934.27,726.996,-1.38281,0.00195312,0.916016,3,3,3,3,0,764.558,446.811,-1.98249,6442.82,8.04375 +1952095,0.205875,0.885293,0.326777,-2.51125,-2.63375,-18.8388,0.15442,0.11046,-0.57176,41.4878,-89.5062,183.826,8,1,33.37,934.27,726.996,-1.38281,0.00195312,0.916016,3,3,3,3,0,764.558,446.811,-1.98249,6442.82,8.04375 +1952105,0.182878,1.21524,-0.432673,-2.33625,-4.515,-7.83125,0.13972,0.03528,-0.57442,41.4878,-89.5062,183.826,8,1,33.37,932.95,739.674,-1.29297,-0.00976562,0.695312,3,3,3,3,0,764.558,446.811,-1.98249,6442.82,8.02441 +1952115,0.182878,1.21524,-0.432673,-2.33625,-4.515,-7.83125,0.13972,0.03528,-0.57442,41.4878,-89.5062,183.826,8,1,33.37,932.95,739.674,-1.29297,-0.00976562,0.695312,3,3,3,3,0,764.558,446.811,-1.98249,6442.82,8.02441 +1952125,-0.111874,1.15973,-0.832467,-7.14875,1.72375,9.4675,0.1449,-0.03682,-0.5838,41.4878,-89.5062,183.826,8,1,33.37,931.95,749.29,-1.29297,-0.00976562,0.695312,3,3,3,4,0,764.558,446.811,-1.98249,6442.82,8.02441 +1952135,-0.111874,1.15973,-0.832467,-7.14875,1.72375,9.4675,0.1449,-0.03682,-0.5838,41.4878,-89.5062,183.826,8,1,33.37,931.95,749.29,-1.35352,-0.0175781,-0.263672,3,3,3,4,0,793.455,446.17,-11.8641,6471.56,8.01475 +1952145,-0.111874,1.15973,-0.832467,-7.14875,1.72375,9.4675,0.1449,-0.03682,-0.5838,41.4878,-89.5062,183.826,8,1,33.37,931.95,749.29,-1.35352,-0.0175781,-0.263672,3,3,3,4,0,793.455,446.17,-11.8641,6471.56,8.01475 +1952155,0.301279,0.852658,-1.53537,-1.855,7.42,-20.7375,0.25004,-0.13706,-0.59808,41.4878,-89.5062,183.826,8,1,33.34,930.62,762.021,-1.35352,-0.0175781,-0.263672,4,4,4,4,0,793.455,446.17,-11.8641,6471.56,8.05342 +1952165,0.301279,0.852658,-1.53537,-1.855,7.42,-20.7375,0.25004,-0.13706,-0.59808,41.4878,-89.5062,183.826,8,1,33.34,930.62,762.021,-1.35352,-0.0175781,-0.263672,4,4,4,4,0,793.455,446.17,-11.8641,6471.56,8.05342 +1952175,0.301279,0.852658,-1.53537,-1.855,7.42,-20.7375,0.25004,-0.13706,-0.59808,41.4878,-89.5062,183.826,8,1,33.34,930.62,762.021,-1.26953,0.0136719,-0.648438,4,4,4,4,0,793.455,446.17,-11.8641,6471.56,8.05342 +1952185,0.301279,0.852658,-1.53537,-1.855,7.42,-20.7375,0.25004,-0.13706,-0.59808,41.4878,-89.5062,183.826,8,1,33.34,930.62,762.021,-1.26953,0.0136719,-0.648438,4,4,4,4,0,793.455,446.17,-11.8641,6471.56,8.05342 +1952195,0.17568,0.98576,-1.94547,-4.96125,0.2625,4.15625,0.32004,-0.15274,-0.59248,41.4878,-89.5062,183.826,8,1,33.35,929.65,771.397,-1.21289,-0.0605469,-1.04883,4,4,4,4,0,816.408,444.891,-26.8672,6494.12,8.07275 +1952205,0.17568,0.98576,-1.94547,-4.96125,0.2625,4.15625,0.32004,-0.15274,-0.59248,41.4878,-89.5062,183.826,8,1,33.35,929.65,771.397,-1.21289,-0.0605469,-1.04883,4,4,4,4,0,816.408,444.891,-26.8672,6494.12,8.07275 +1952215,0.17568,0.98576,-1.94547,-4.96125,0.2625,4.15625,0.32004,-0.15274,-0.59248,41.4878,-89.5062,183.826,8,1,33.35,928.72,780.371,-1.21289,-0.0605469,-1.04883,4,4,4,4,0,816.408,444.891,-26.8672,6494.12,8.07275 +1952225,-0.02257,0.827465,-1.9986,-2.77375,-4.3925,-10.675,0.38234,-0.14266,-0.5922,41.4878,-89.5062,183.965,9,1,33.35,928.72,780.371,-1.28516,-0.0800781,-1.88281,4,4,4,4,0,816.408,444.891,-26.8672,6494.12,8.07275 +1952235,-0.02257,0.827465,-1.9986,-2.77375,-4.3925,-10.675,0.38234,-0.14266,-0.5922,41.4878,-89.5062,183.965,9,1,33.35,928.72,780.371,-1.28516,-0.0800781,-1.88281,4,4,4,4,0,816.408,444.891,-26.8672,6494.12,8.07275 +1952245,0.256078,0.869433,-1.9986,-3.6925,-9.56375,-4.3575,0.43764,-0.10794,-0.57778,41.4878,-89.5062,183.965,9,1,33.35,927.91,788.194,-1.27344,-0.0605469,-2.22656,4,4,4,4,0,839.15,443.067,-34.0395,6495.38,8.07275 +1952255,0.256078,0.869433,-1.9986,-3.6925,-9.56375,-4.3575,0.43764,-0.10794,-0.57778,41.4878,-89.5062,183.965,9,1,33.35,927.91,788.194,-1.27344,-0.0605469,-2.22656,4,4,4,4,0,839.15,443.067,-34.0395,6495.38,8.07275 +1952265,0.256078,0.869433,-1.9986,-3.6925,-9.56375,-4.3575,0.43764,-0.10794,-0.57778,41.4878,-89.5062,183.965,9,1,33.35,927.91,788.194,-1.27344,-0.0605469,-2.22656,4,4,4,4,0,839.15,443.067,-34.0395,6495.38,8.07275 +1952275,-0.145058,1.18096,-1.9986,-4.0425,1.2775,-5.20625,0.46942,-0.04284,-0.57274,41.4878,-89.5062,183.965,9,1,33.35,927.09,796.122,-1.26953,-0.09375,-2.51172,4,4,4,4,0,839.15,443.067,-34.0395,6495.38,8.06309 +1952285,-0.145058,1.18096,-1.9986,-4.0425,1.2775,-5.20625,0.46942,-0.04284,-0.57274,41.4878,-89.5062,183.965,9,1,33.35,927.09,796.122,-1.26953,-0.09375,-2.51172,4,4,4,4,0,839.15,443.067,-34.0395,6495.38,8.06309 +1952295,0.047275,0.461892,-1.9986,-3.57,15.5138,-4.935,0.47054,0.03598,-0.5705,41.4878,-89.5062,183.965,9,1,33.35,926.15,805.218,-1.26953,-0.09375,-2.51172,4,4,4,4,0,839.15,443.067,-34.0395,6495.38,8.06309 +1952305,0.047275,0.461892,-1.9986,-3.57,15.5138,-4.935,0.47054,0.03598,-0.5705,41.4878,-89.5062,183.965,9,1,33.35,926.15,805.218,-1.26367,0.0253906,-3.0332,4,4,4,4,0,861.708,440.853,-41.1384,6484.89,8.05342 +1952315,0.047275,0.461892,-1.9986,-3.57,15.5138,-4.935,0.47054,0.03598,-0.5705,41.4878,-89.5062,183.965,9,1,33.35,926.15,805.218,-1.26367,0.0253906,-3.0332,4,4,4,4,0,861.708,440.853,-41.1384,6484.89,8.05342 +1952325,0.186355,0.949343,-1.9986,-3.6575,-10.15,-5.20625,0.44044,0.09926,-0.56378,41.4878,-89.5062,183.965,9,1,33.35,925.19,814.516,-1.10742,0.0507812,-3.23438,4,4,4,4,0,861.708,440.853,-41.1384,6484.89,8.03408 +1952335,0.186355,0.949343,-1.9986,-3.6575,-10.15,-5.20625,0.44044,0.09926,-0.56378,41.4878,-89.5062,183.965,9,1,33.35,925.19,814.516,-1.10742,0.0507812,-3.23438,4,4,4,4,0,861.708,440.853,-41.1384,6484.89,8.03408 +1952345,0.186355,0.949343,-1.9986,-3.6575,-10.15,-5.20625,0.44044,0.09926,-0.56378,41.4878,-89.5062,183.965,9,1,33.35,924.23,823.825,-1.10742,0.0507812,-3.23438,4,4,4,4,0,861.708,440.853,-41.1384,6484.89,8.03408 +1952355,0.05185,0.672647,-1.9986,-3.21125,-2.1175,-13.825,0.3969,0.15344,-0.56378,41.4878,-89.5062,183.965,9,1,33.35,924.23,823.825,-1.0918,0.00390625,-3.34961,4,4,4,4,0,884.136,438.418,-43.7298,6466.36,8.08242 +1952365,0.05185,0.672647,-1.9986,-3.21125,-2.1175,-13.825,0.3969,0.15344,-0.56378,41.4878,-89.5062,183.965,9,1,33.35,924.23,823.825,-1.0918,0.00390625,-3.34961,4,4,4,4,0,884.136,438.418,-43.7298,6466.36,8.08242 +1952375,0.025681,0.705831,-1.9986,-4.2875,4.82125,-0.6125,0.31738,0.18004,-0.55244,41.4878,-89.5062,183.965,9,1,33.35,922.91,836.64,-1.11719,0.03125,-3.43945,4,4,4,4,0,884.136,438.418,-43.7298,6466.36,8.02441 +1952385,0.025681,0.705831,-1.9986,-4.2875,4.82125,-0.6125,0.31738,0.18004,-0.55244,41.4878,-89.5062,183.965,9,1,33.35,922.91,836.64,-1.11719,0.03125,-3.43945,4,4,4,4,0,884.136,438.418,-43.7298,6466.36,8.02441 +1952395,0.025681,0.705831,-1.9986,-4.2875,4.82125,-0.6125,0.31738,0.18004,-0.55244,41.4878,-89.5062,183.965,9,1,33.35,922.91,836.64,-1.11719,0.03125,-3.43945,4,4,4,4,0,884.136,438.418,-43.7298,6466.36,8.02441 +1952405,0.015006,0.936838,-1.9986,-4.80375,2.275,0.04375,0.25522,0.1729,-0.56042,41.4878,-89.5062,183.965,9,1,33.35,921.52,850.154,-1.08594,0.0078125,-3.48633,4,4,4,4,0,906.463,435.903,-44.8569,6443.93,8.06309 +1952415,0.015006,0.936838,-1.9986,-4.80375,2.275,0.04375,0.25522,0.1729,-0.56042,41.4878,-89.5062,183.965,9,1,33.35,921.52,850.154,-1.08594,0.0078125,-3.48633,4,4,4,4,0,906.463,435.903,-44.8569,6443.93,8.06309 +1952425,0.015006,0.936838,-1.9986,-4.80375,2.275,0.04375,0.25522,0.1729,-0.56042,41.4878,-89.5062,183.965,9,1,33.35,920.45,860.572,-1.08594,0.0078125,-3.48633,4,4,4,5,0,906.463,435.903,-44.8569,6443.93,8.06309 +1952435,-0.063257,0.773419,-1.9986,-6.41375,-7.30625,7.735,0.19376,0.1365,-0.57064,41.4878,-89.5062,184.107,9,1,33.35,920.45,860.572,-1.09375,-0.0292969,-3.53516,4,4,4,5,0,906.463,435.903,-44.8569,6443.93,8.05342 +1952445,-0.063257,0.773419,-1.9986,-6.41375,-7.30625,7.735,0.19376,0.1365,-0.57064,41.4878,-89.5062,184.107,9,1,33.35,920.45,860.572,-1.09375,-0.0292969,-3.53516,4,4,4,5,0,906.463,435.903,-44.8569,6443.93,8.05342 +1952455,0.143411,0.642452,-1.9986,-0.6125,7.84,-20.58,0.14854,0.04788,-0.57442,41.4878,-89.5062,184.107,9,1,33.35,919.56,869.246,-1.05859,0.00976562,-3.5918,4,4,4,5,0,928.582,433.327,-45.7155,6420.32,8.03408 +1952465,0.143411,0.642452,-1.9986,-0.6125,7.84,-20.58,0.14854,0.04788,-0.57442,41.4878,-89.5062,184.107,9,1,33.35,919.56,869.246,-1.05859,0.00976562,-3.5918,4,4,4,5,0,928.582,433.327,-45.7155,6420.32,8.03408 +1952475,0.143411,0.642452,-1.9986,-0.6125,7.84,-20.58,0.14854,0.04788,-0.57442,41.4878,-89.5062,184.107,9,1,33.35,919.56,869.246,-1.05859,0.00976562,-3.5918,4,4,4,5,0,928.582,433.327,-45.7155,6420.32,8.03408 +1952485,0.145607,0.745176,-1.9986,-4.9525,11.445,1.30375,0.15134,-0.0266,-0.5796,41.4878,-89.5062,184.107,9,1,33.35,918.59,878.709,-1.00391,-0.0117188,-3.61914,5,5,5,5,0,928.582,433.327,-45.7155,6420.32,8.05342 +1952495,0.145607,0.745176,-1.9986,-4.9525,11.445,1.30375,0.15134,-0.0266,-0.5796,41.4878,-89.5062,184.107,9,1,33.35,918.59,878.709,-1.00391,-0.0117188,-3.61914,5,5,5,5,0,928.582,433.327,-45.7155,6420.32,8.05342 +1952505,-0.043249,0.693753,-1.9986,-2.73,3.6925,-6.685,0.17948,-0.10038,-0.58044,41.4878,-89.5062,184.107,9,1,33.35,917.52,889.159,-0.998047,-0.046875,-3.63477,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.01475 +1952515,-0.043249,0.693753,-1.9986,-2.73,3.6925,-6.685,0.17948,-0.10038,-0.58044,41.4878,-89.5062,184.107,9,1,33.35,917.52,889.159,-0.998047,-0.046875,-3.63477,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.01475 +1952525,-0.043249,0.693753,-1.9986,-2.73,3.6925,-6.685,0.17948,-0.10038,-0.58044,41.4878,-89.5062,184.107,9,1,33.35,917.52,889.159,-0.998047,-0.046875,-3.63477,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.01475 +1952535,0.064843,0.602863,-1.9986,-3.9375,5.0575,-7.91875,0.2359,-0.1449,-0.57764,41.4878,-89.5062,184.107,9,1,33.35,916.31,900.991,-0.984375,-0.046875,-3.64844,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.03408 +1952545,0.064843,0.602863,-1.9986,-3.9375,5.0575,-7.91875,0.2359,-0.1449,-0.57764,41.4878,-89.5062,184.107,9,1,33.35,916.31,900.991,-0.984375,-0.046875,-3.64844,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.03408 +1952555,0.064843,0.602863,-1.9986,-3.9375,5.0575,-7.91875,0.2359,-0.1449,-0.57764,41.4878,-89.5062,184.107,9,1,33.32,914.4,919.611,-0.984375,-0.046875,-3.64844,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.03408 +1952565,0.071248,0.64111,-1.9986,-2.6425,2.09125,-14.1225,0.34776,-0.15792,-0.58632,41.4878,-89.5062,184.107,9,1,33.32,914.4,919.611,-0.984375,-0.046875,-3.64844,5,5,5,5,0,958.202,429.823,-46.0787,6395.83,8.04375 +1952575,0.071248,0.64111,-1.9986,-2.6425,2.09125,-14.1225,0.34776,-0.15792,-0.58632,41.4878,-89.5062,184.107,9,1,33.32,914.4,919.611,-0.994141,-0.0566406,-3.66406,5,5,5,5,0,979.169,427.268,-46.3293,6363.42,8.04375 +1952585,0.071248,0.64111,-1.9986,-2.6425,2.09125,-14.1225,0.34776,-0.15792,-0.58632,41.4878,-89.5062,184.107,9,1,33.32,914.4,919.611,-0.994141,-0.0566406,-3.66406,5,5,5,5,0,979.169,427.268,-46.3293,6363.42,8.04375 +1952595,0.071248,0.64111,-1.9986,-2.6425,2.09125,-14.1225,0.34776,-0.15792,-0.58632,41.4878,-89.5062,184.107,9,1,33.32,913.21,931.286,-0.994141,-0.0566406,-3.66406,5,5,5,5,0,979.169,427.268,-46.3293,6363.42,8.04375 +1952605,0.09516,0.502457,-1.9986,-3.54375,13.2388,-7.18375,0.4102,-0.12754,-0.57736,41.4878,-89.5062,184.107,9,1,33.32,913.21,931.286,-1.01758,-0.0605469,-3.67383,5,5,5,5,0,979.169,427.268,-46.3293,6363.42,8.06309 +1952615,0.09516,0.502457,-1.9986,-3.54375,13.2388,-7.18375,0.4102,-0.12754,-0.57736,41.4878,-89.5062,184.107,9,1,33.32,913.21,931.286,-1.01758,-0.0605469,-3.67383,5,5,5,5,0,979.169,427.268,-46.3293,6363.42,8.06309 +1952625,0.093696,0.65026,-1.9986,-0.9625,4.33125,-13.1075,0.45682,-0.0679,-0.5761,41.4878,-89.5062,184.248,8,1,33.33,912.31,940.157,-0.990234,-0.0351562,-3.67578,5,5,5,5,0,1000.38,424.657,-46.3586,6339.52,8.05342 +1952635,0.093696,0.65026,-1.9986,-0.9625,4.33125,-13.1075,0.45682,-0.0679,-0.5761,41.4878,-89.5062,184.248,8,1,33.33,912.31,940.157,-0.990234,-0.0351562,-3.67578,5,5,5,5,0,1000.38,424.657,-46.3586,6339.52,8.05342 +1952645,0.093696,0.65026,-1.9986,-0.9625,4.33125,-13.1075,0.45682,-0.0679,-0.5761,41.4878,-89.5062,184.248,8,1,33.33,912.31,940.157,-0.990234,-0.0351562,-3.67578,5,5,5,5,0,1000.38,424.657,-46.3586,6339.52,8.05342 +1952655,0.079666,0.641598,-1.9986,-7.07,1.09375,16.1,0.46074,0.00994,-0.57036,41.4878,-89.5062,184.248,8,1,33.33,911.31,949.989,-0.966797,-0.0507812,-3.67383,5,5,5,5,0,1000.38,424.657,-46.3586,6339.52,8.06309 +1952665,0.079666,0.641598,-1.9986,-7.07,1.09375,16.1,0.46074,0.00994,-0.57036,41.4878,-89.5062,184.248,8,1,33.33,911.31,949.989,-0.966797,-0.0507812,-3.67383,5,5,5,5,0,1000.38,424.657,-46.3586,6339.52,8.06309 +1952675,0.079666,0.641598,-1.9986,-7.07,1.09375,16.1,0.46074,0.00994,-0.57036,41.4878,-89.5062,184.248,8,1,33.33,910.24,960.523,-0.966797,-0.0507812,-3.67383,5,5,5,5,0,1000.38,424.657,-46.3586,6339.52,8.06309 +1952685,0.021716,0.5795,-1.9986,-5.635,8.645,-0.805,0.43008,0.11074,-0.57078,41.4878,-89.5062,184.248,8,1,33.33,910.24,960.523,-0.949219,-0.0605469,-3.68164,5,5,5,5,0,1021.88,421.999,-46.5148,6315.69,8.08242 +1952695,0.021716,0.5795,-1.9986,-5.635,8.645,-0.805,0.43008,0.11074,-0.57078,41.4878,-89.5062,184.248,8,1,33.33,910.24,960.523,-0.949219,-0.0605469,-3.68164,5,5,5,5,0,1021.88,421.999,-46.5148,6315.69,8.08242 +1952705,-0.051789,0.53985,-1.9986,-2.9575,4.4275,-9.0125,0.37128,0.16366,-0.56644,41.4878,-89.5062,184.248,8,1,33.33,909.12,971.562,-0.925781,-0.0996094,-3.69141,5,5,5,5,0,1021.88,421.999,-46.5148,6315.69,8.07275 +1952715,-0.051789,0.53985,-1.9986,-2.9575,4.4275,-9.0125,0.37128,0.16366,-0.56644,41.4878,-89.5062,184.248,8,1,33.33,909.12,971.562,-0.925781,-0.0996094,-3.69141,5,5,5,5,0,1021.88,421.999,-46.5148,6315.69,8.07275 +1952725,-0.051789,0.53985,-1.9986,-2.9575,4.4275,-9.0125,0.37128,0.16366,-0.56644,41.4878,-89.5062,184.248,8,1,33.33,908.05,982.12,-0.925781,-0.0996094,-3.69141,5,5,5,5,0,1021.88,421.999,-46.5148,6315.69,8.07275 +1952735,0.083082,0.577548,-1.9986,-4.03375,6.67625,-2.2925,0.29708,0.18396,-0.57638,41.4878,-89.5062,184.248,8,1,33.33,908.05,982.12,-0.925781,-0.0976562,-3.7168,5,5,5,5,0,1043.27,419.332,-46.9909,6291.76,8.06309 +1952745,0.083082,0.577548,-1.9986,-4.03375,6.67625,-2.2925,0.29708,0.18396,-0.57638,41.4878,-89.5062,184.248,8,1,33.33,908.05,982.12,-0.925781,-0.0976562,-3.7168,5,5,5,5,0,1043.27,419.332,-46.9909,6291.76,8.06309 +1952755,0.040321,0.443531,-1.9986,-2.485,0.14,-7.34125,0.23016,0.16478,-0.58408,41.4878,-89.5062,184.248,8,1,33.33,906.83,994.175,-0.935547,-0.0898438,-3.74414,5,5,5,5,0,1043.27,419.332,-46.9909,6291.76,8.08242 +1952765,0.040321,0.443531,-1.9986,-2.485,0.14,-7.34125,0.23016,0.16478,-0.58408,41.4878,-89.5062,184.248,8,1,33.33,906.83,994.175,-0.935547,-0.0898438,-3.74414,5,5,5,5,0,1043.27,419.332,-46.9909,6291.76,8.08242 +1952775,0.040321,0.443531,-1.9986,-2.485,0.14,-7.34125,0.23016,0.16478,-0.58408,41.4878,-89.5062,184.248,8,1,33.33,906.83,994.175,-0.935547,-0.0898438,-3.74414,5,5,5,5,0,1043.27,419.332,-46.9909,6291.76,8.08242 +1952785,0.18788,0.525942,-1.9986,-4.3925,-3.5175,-3.3075,0.16828,0.10794,-0.57232,41.4878,-89.5062,184.248,8,1,33.33,905.85,1003.87,-0.878906,-0.0605469,-3.75391,5,5,5,5,0,1064.48,416.656,-46.9943,6267.94,8.01475 +1952795,0.18788,0.525942,-1.9986,-4.3925,-3.5175,-3.3075,0.16828,0.10794,-0.57232,41.4878,-89.5062,184.248,8,1,33.33,905.85,1003.87,-0.878906,-0.0605469,-3.75391,5,5,5,5,0,1064.48,416.656,-46.9943,6267.94,8.01475 +1952805,0.18788,0.525942,-1.9986,-4.3925,-3.5175,-3.3075,0.16828,0.10794,-0.57232,41.4878,-89.5062,184.248,8,1,33.33,904.8,1014.27,-0.878906,-0.0605469,-3.75391,5,5,5,5,0,1064.48,416.656,-46.9943,6267.94,8.01475 +1952815,-0.049532,0.515938,-1.9986,-3.4825,-12.5475,-6.265,0.14546,0.03864,-0.5663,41.4878,-89.5062,184.248,8,1,33.33,904.8,1014.27,-0.873047,-0.0996094,-3.73828,5,5,5,5,0,1064.48,416.656,-46.9943,6267.94,8.05342 +1952825,-0.049532,0.515938,-1.9986,-3.4825,-12.5475,-6.265,0.14546,0.03864,-0.5663,41.4878,-89.5062,184.248,8,1,33.33,904.8,1014.27,-0.873047,-0.0996094,-3.73828,5,5,5,5,0,1064.48,416.656,-46.9943,6267.94,8.05342 +1952835,0.341051,0.44103,-1.9986,-4.585,-2.5725,-3.4125,0.1512,-0.04088,-0.57904,41.4878,-89.5062,184.389,7,1,33.33,903.71,1025.07,-0.876953,-0.0644531,-3.7207,5,5,5,5,0,1085.19,414.05,-46.7573,6244.63,8.02441 +1952845,0.341051,0.44103,-1.9986,-4.585,-2.5725,-3.4125,0.1512,-0.04088,-0.57904,41.4878,-89.5062,184.389,7,1,33.33,903.71,1025.07,-0.876953,-0.0644531,-3.7207,5,5,5,5,0,1085.19,414.05,-46.7573,6244.63,8.02441 +1952855,0.341051,0.44103,-1.9986,-4.585,-2.5725,-3.4125,0.1512,-0.04088,-0.57904,41.4878,-89.5062,184.389,7,1,33.33,903.71,1025.07,-0.876953,-0.0644531,-3.7207,5,5,5,5,0,1085.19,414.05,-46.7573,6244.63,8.02441 +1952865,0.125233,0.556747,-1.9986,-3.36,-4.57625,-11.55,0.1841,-0.11158,-0.58296,41.4878,-89.5062,184.389,7,1,33.33,902.45,1037.58,-0.851562,-0.107422,-3.71094,5,5,5,5,0,1085.19,414.05,-46.7573,6244.63,8.02441 +1952875,0.125233,0.556747,-1.9986,-3.36,-4.57625,-11.55,0.1841,-0.11158,-0.58296,41.4878,-89.5062,184.389,7,1,33.33,902.45,1037.58,-0.851562,-0.107422,-3.71094,5,5,5,5,0,1085.19,414.05,-46.7573,6244.63,8.02441 +1952885,0.149084,0.465613,-1.9986,-1.95125,4.48,-11.7513,0.24598,-0.1554,-0.5838,41.4878,-89.5062,184.389,7,1,33.33,901.14,1050.61,-0.859375,-0.125,-3.69922,5,5,5,5,0,1106.24,411.424,-46.5091,6222.24,8.03408 +1952895,0.149084,0.465613,-1.9986,-1.95125,4.48,-11.7513,0.24598,-0.1554,-0.5838,41.4878,-89.5062,184.389,7,1,33.33,901.14,1050.61,-0.859375,-0.125,-3.69922,5,5,5,5,0,1106.24,411.424,-46.5091,6222.24,8.03408 +1952905,0.149084,0.465613,-1.9986,-1.95125,4.48,-11.7513,0.24598,-0.1554,-0.5838,41.4878,-89.5062,184.389,7,1,33.33,901.14,1050.61,-0.859375,-0.125,-3.69922,5,5,5,5,0,1106.24,411.424,-46.5091,6222.24,8.03408 +1952915,0.038857,0.421083,-1.9986,-3.01,16.7913,-13.72,0.31766,-0.16982,-0.57442,41.4878,-89.5062,184.389,7,1,33.33,900.02,1061.76,-0.867188,-0.0703125,-3.68555,5,5,5,5,0,1106.24,411.424,-46.5091,6222.24,8.03408 +1952925,0.038857,0.421083,-1.9986,-3.01,16.7913,-13.72,0.31766,-0.16982,-0.57442,41.4878,-89.5062,184.389,7,1,33.33,900.02,1061.76,-0.867188,-0.0703125,-3.68555,5,5,5,5,0,1106.24,411.424,-46.5091,6222.24,8.03408 +1952935,0.038857,0.421083,-1.9986,-3.01,16.7913,-13.72,0.31766,-0.16982,-0.57442,41.4878,-89.5062,184.389,7,1,33.33,899.04,1071.53,-0.867188,-0.0703125,-3.68555,5,5,5,5,0,1106.24,411.424,-46.5091,6222.24,8.03408 +1952945,0.109373,0.365939,-1.9986,-1.40875,0.7175,-15.0763,0.38514,-0.15414,-0.57064,41.4878,-89.5062,184.389,7,1,33.33,899.04,1071.53,-0.814453,-0.0859375,-3.64844,5,5,5,5,0,1132.82,408.128,-46.1511,6200.46,8.04375 +1952955,0.109373,0.365939,-1.9986,-1.40875,0.7175,-15.0763,0.38514,-0.15414,-0.57064,41.4878,-89.5062,184.389,7,1,33.33,899.04,1071.53,-0.814453,-0.0859375,-3.64844,5,5,5,5,0,1132.82,408.128,-46.1511,6200.46,8.04375 +1952965,0.186477,0.50325,-1.9986,-3.64,-3.45625,-2.86125,0.46494,-0.0175,-0.52136,41.4878,-89.5062,184.389,7,1,33.31,897.69,1084.93,-0.814453,-0.0859375,-3.64844,5,5,5,5,0,1132.82,408.128,-46.1511,6200.46,8.05342 +1952975,0.186477,0.50325,-1.9986,-3.64,-3.45625,-2.86125,0.46494,-0.0175,-0.52136,41.4878,-89.5062,184.389,7,1,33.31,897.69,1084.93,-0.814453,-0.0859375,-3.64844,5,5,5,5,0,1132.82,408.128,-46.1511,6200.46,8.05342 +1952985,0.186477,0.50325,-1.9986,-3.64,-3.45625,-2.86125,0.46494,-0.0175,-0.52136,41.4878,-89.5062,184.389,7,1,33.31,897.69,1084.93,-0.794922,-0.0957031,-3.63086,5,5,5,5,0,1132.82,408.128,-46.1511,6200.46,8.05342 +1952995,0.186477,0.50325,-1.9986,-3.64,-3.45625,-2.86125,0.46494,-0.0175,-0.52136,41.4878,-89.5062,184.389,7,1,33.31,897.69,1084.93,-0.794922,-0.0957031,-3.63086,5,5,5,5,0,1132.82,408.128,-46.1511,6200.46,8.05342 +1953005,0.20679,0.414922,-1.9986,-4.96125,-2.9575,-3.33375,0.45528,0.06692,-0.56182,41.4878,-89.5062,184.389,7,1,33.31,896.55,1096.32,-0.775391,-0.0957031,-3.60547,5,5,5,5,0,1153.53,405.523,-45.5063,6173.9,8.06309 +1953015,0.20679,0.414922,-1.9986,-4.96125,-2.9575,-3.33375,0.45528,0.06692,-0.56182,41.4878,-89.5062,184.389,7,1,33.31,896.55,1096.32,-0.775391,-0.0957031,-3.60547,5,5,5,5,0,1153.53,405.523,-45.5063,6173.9,8.06309 +1953025,0.20679,0.414922,-1.9986,-4.96125,-2.9575,-3.33375,0.45528,0.06692,-0.56182,41.4878,-89.5062,184.389,7,1,33.31,896.55,1096.32,-0.775391,-0.0957031,-3.60547,5,5,5,5,0,1153.53,405.523,-45.5063,6173.9,8.06309 +1953035,0.099918,0.541009,-1.9986,-3.7275,1.855,-5.3375,0.4102,0.13986,-0.55846,41.4878,-89.5062,184.538,0,0,33.31,895.41,1107.73,-0.753906,-0.162109,-3.57812,5,5,5,5,0,1153.53,405.523,-45.5063,6173.9,8.06309 +1953045,0.099918,0.541009,-1.9986,-3.7275,1.855,-5.3375,0.4102,0.13986,-0.55846,41.4878,-89.5062,184.538,0,0,33.31,895.41,1107.73,-0.753906,-0.162109,-3.57812,5,5,5,5,0,1153.53,405.523,-45.5063,6173.9,8.06309 +1953055,0.099918,0.541009,-1.9986,-3.7275,1.855,-5.3375,0.4102,0.13986,-0.55846,41.4878,-89.5062,184.538,0,0,33.31,894.22,1119.65,-0.753906,-0.162109,-3.57812,5,5,5,5,0,1153.53,405.523,-45.5063,6173.9,8.06309 +1953065,0.01647,0.284687,-1.9986,-3.42125,2.9925,-4.06875,0.34748,0.17906,-0.5628,41.4878,-89.5062,184.538,0,0,33.31,894.22,1119.65,-0.787109,-0.132812,-3.56055,5,5,5,5,0,1175.32,402.806,-45.2789,6152.87,8.03408 +1953075,0.01647,0.284687,-1.9986,-3.42125,2.9925,-4.06875,0.34748,0.17906,-0.5628,41.4878,-89.5062,184.538,0,0,33.31,894.22,1119.65,-0.787109,-0.132812,-3.56055,5,5,5,5,0,1175.32,402.806,-45.2789,6152.87,8.03408 +1953085,0.106872,0.419253,-1.9986,-2.09125,5.01375,-14.4113,0.2744,0.18564,-0.57232,41.4878,-89.5062,184.538,0,0,33.31,893.12,1130.69,-0.787109,-0.132812,-3.56055,5,5,5,5,0,1175.32,402.806,-45.2789,6152.87,8.05342 +1953095,0.106872,0.419253,-1.9986,-2.09125,5.01375,-14.4113,0.2744,0.18564,-0.57232,41.4878,-89.5062,184.538,0,0,33.31,893.12,1130.69,-0.757812,-0.0957031,-3.53125,5,5,5,5,0,1175.32,402.806,-45.2789,6152.87,8.05342 +1953105,0.106872,0.419253,-1.9986,-2.09125,5.01375,-14.4113,0.2744,0.18564,-0.57232,41.4878,-89.5062,184.538,0,0,33.31,893.12,1130.69,-0.757812,-0.0957031,-3.53125,5,5,5,5,0,1175.32,402.806,-45.2789,6152.87,8.05342 +1953115,0.144814,0.222162,-1.9986,-1.70625,6.39625,-16.73,0.20748,0.15344,-0.58128,41.4878,-89.5062,184.538,0,0,33.31,892.19,1140.03,-0.769531,-0.101562,-3.51758,5,5,5,5,0,1194.96,400.344,-44.7468,6131.83,8.07275 +1953125,0.144814,0.222162,-1.9986,-1.70625,6.39625,-16.73,0.20748,0.15344,-0.58128,41.4878,-89.5062,184.538,0,0,33.31,892.19,1140.03,-0.769531,-0.101562,-3.51758,5,5,5,5,0,1194.96,400.344,-44.7468,6131.83,8.07275 +1953135,0.222833,0.353007,-1.9986,-7.3675,-6.95625,16.24,0.16338,0.09352,-0.58282,41.4878,-89.5062,184.538,0,0,33.31,891.18,1150.18,-0.769531,-0.101562,-3.51758,5,5,5,5,0,1194.96,400.344,-44.7468,6131.83,8.07275 +1953145,0.222833,0.353007,-1.9986,-7.3675,-6.95625,16.24,0.16338,0.09352,-0.58282,41.4878,-89.5062,184.538,0,0,33.31,891.18,1150.18,-0.751953,-0.0957031,-3.50195,5,5,5,6,0,1194.96,400.344,-44.7468,6131.83,8.05342 +1953155,0.222833,0.353007,-1.9986,-7.3675,-6.95625,16.24,0.16338,0.09352,-0.58282,41.4878,-89.5062,184.538,0,0,33.31,891.18,1150.18,-0.751953,-0.0957031,-3.50195,5,5,5,6,0,1194.96,400.344,-44.7468,6131.83,8.05342 +1953165,0.157807,0.379542,-1.9986,-6.125,5.36375,14.3675,0.15834,-0.0238,-0.59346,41.4878,-89.5062,184.538,0,0,33.31,890.08,1161.25,-0.730469,-0.136719,-3.46484,5,5,5,6,0,1215.31,397.833,-44.2257,6112.82,8.08242 +1953175,0.157807,0.379542,-1.9986,-6.125,5.36375,14.3675,0.15834,-0.0238,-0.59346,41.4878,-89.5062,184.538,0,0,33.31,890.08,1161.25,-0.730469,-0.136719,-3.46484,5,5,5,6,0,1215.31,397.833,-44.2257,6112.82,8.08242 +1953185,0.157807,0.379542,-1.9986,-6.125,5.36375,14.3675,0.15834,-0.0238,-0.59346,41.4878,-89.5062,184.538,0,0,33.32,888.93,1172.88,-0.730469,-0.136719,-3.46484,5,5,5,6,0,1215.31,397.833,-44.2257,6112.82,8.08242 +1953195,0.032513,0.352702,-1.9986,-6.895,4.48875,9.75625,0.1799,-0.09954,-0.57624,41.4878,-89.5062,184.538,0,0,33.32,888.93,1172.88,-0.734375,-0.144531,-3.44922,6,6,6,6,0,1215.31,397.833,-44.2257,6112.82,8.01475 +1953205,0.032513,0.352702,-1.9986,-6.895,4.48875,9.75625,0.1799,-0.09954,-0.57624,41.4878,-89.5062,184.538,0,0,33.32,888.93,1172.88,-0.734375,-0.144531,-3.44922,6,6,6,6,0,1215.31,397.833,-44.2257,6112.82,8.01475 +1953215,0.098515,0.358009,-1.9986,-4.48875,6.53625,0.735,0.24038,-0.15176,-0.57736,41.4878,-89.5062,184.538,0,0,33.32,887.86,1183.68,-0.740234,-0.121094,-3.43359,6,6,6,6,0,1235.55,395.355,-43.7418,6094.04,8.00508 +1953225,0.098515,0.358009,-1.9986,-4.48875,6.53625,0.735,0.24038,-0.15176,-0.57736,41.4878,-89.5062,184.538,0,0,33.32,887.86,1183.68,-0.740234,-0.121094,-3.43359,6,6,6,6,0,1235.55,395.355,-43.7418,6094.04,8.00508 +1953235,0.098515,0.358009,-1.9986,-4.48875,6.53625,0.735,0.24038,-0.15176,-0.57736,41.4878,-89.5062,184.538,0,0,33.32,887.86,1183.68,-0.740234,-0.121094,-3.43359,6,6,6,6,0,1235.55,395.355,-43.7418,6094.04,8.00508 +1953245,-0.083082,0.41114,-1.9986,-3.84125,2.10875,-4.2875,0.10318,0.32074,-0.04438,41.4878,-89.5062,184.687,0,0,33.32,886.83,1194.09,-0.710938,-0.121094,-3.40039,6,6,6,6,0,1235.55,395.355,-43.7418,6094.04,7.396 +1953255,-0.083082,0.41114,-1.9986,-3.84125,2.10875,-4.2875,0.10318,0.32074,-0.04438,41.4878,-89.5062,184.687,0,0,33.32,886.83,1194.09,-0.710938,-0.121094,-3.40039,6,6,6,6,0,1235.55,395.355,-43.7418,6094.04,7.396 +1953265,-0.083082,0.41114,-1.9986,-3.84125,2.10875,-4.2875,0.10318,0.32074,-0.04438,41.4878,-89.5062,184.687,0,0,33.32,885.9,1203.49,-0.710938,-0.121094,-3.40039,6,6,6,6,0,1235.55,395.355,-43.7418,6094.04,7.396 +1953275,0.152378,0.088877,-1.9986,-4.52375,1.155,-2.89625,0.23198,0.12684,-0.23226,41.4878,-89.5062,184.687,0,0,33.32,885.9,1203.49,-0.714844,-0.126953,-3.39062,6,6,6,6,0,1255.45,392.859,-43.5004,6076.23,7.99541 +1953285,0.152378,0.088877,-1.9986,-4.52375,1.155,-2.89625,0.23198,0.12684,-0.23226,41.4878,-89.5062,184.687,0,0,33.32,885.9,1203.49,-0.714844,-0.126953,-3.39062,6,6,6,6,0,1255.45,392.859,-43.5004,6076.23,7.99541 +1953295,0.186904,0.40382,-1.9986,-4.095,1.44375,-6.53625,0.37898,0.01442,-0.4158,41.4878,-89.5062,184.687,0,0,33.32,885.97,1202.79,-0.699219,-0.134766,-3.37891,6,6,6,6,0,1255.45,392.859,-43.5004,6076.23,7.79238 +1953305,0.186904,0.40382,-1.9986,-4.095,1.44375,-6.53625,0.37898,0.01442,-0.4158,41.4878,-89.5062,184.687,0,0,33.32,885.97,1202.79,-0.699219,-0.134766,-3.37891,6,6,6,6,0,1255.45,392.859,-43.5004,6076.23,7.79238 +1953315,0.186904,0.40382,-1.9986,-4.095,1.44375,-6.53625,0.37898,0.01442,-0.4158,41.4878,-89.5062,184.687,0,0,33.32,885.97,1202.79,-0.699219,-0.134766,-3.37891,6,6,6,6,0,1255.45,392.859,-43.5004,6076.23,7.79238 +1953325,0.545401,0.253272,-1.9986,-4.26125,-4.69875,-5.25,0.41888,0.0973,-0.43512,41.4878,-89.5062,184.687,0,0,33.32,926.76,799.236,-0.708984,-0.167969,-3.36719,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.81172 +1953335,0.545401,0.253272,-1.9986,-4.26125,-4.69875,-5.25,0.41888,0.0973,-0.43512,41.4878,-89.5062,184.687,0,0,33.32,926.76,799.236,-0.708984,-0.167969,-3.36719,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.81172 +1953345,-0.26108,0.673623,-1.9986,-4.41875,-5.15375,-7.21875,0.39858,0.17668,-0.4263,41.4878,-89.5062,184.687,0,0,33.32,971.81,373.683,-0.75,-0.134766,-3.39648,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.76338 +1953355,-0.26108,0.673623,-1.9986,-4.41875,-5.15375,-7.21875,0.39858,0.17668,-0.4263,41.4878,-89.5062,184.687,0,0,33.32,971.81,373.683,-0.75,-0.134766,-3.39648,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.76338 +1953365,-0.26108,0.673623,-1.9986,-4.41875,-5.15375,-7.21875,0.39858,0.17668,-0.4263,41.4878,-89.5062,184.687,0,0,33.32,971.81,373.683,-0.75,-0.134766,-3.39648,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.76338 +1953375,0.536922,-0.376858,-1.46589,-5.1975,13.3,6.1775,0.31472,0.29554,-0.39816,41.4878,-89.5062,184.687,0,0,33.3,1024.69,-101.346,-0.75,-0.134766,-3.39648,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.72471 +1953385,0.536922,-0.376858,-1.46589,-5.1975,13.3,6.1775,0.31472,0.29554,-0.39816,41.4878,-89.5062,184.687,0,0,33.3,1024.69,-101.346,-0.75,-0.134766,-3.39648,6,6,6,6,0,1265.84,385.965,-43.6748,6057.84,7.72471 +1953395,0.536922,-0.376858,-1.46589,-5.1975,13.3,6.1775,0.31472,0.29554,-0.39816,41.4878,-89.5062,184.687,0,0,33.3,1024.69,-101.346,-0.740234,-0.144531,-3.43164,6,6,6,6,0,1255.38,377.19,-44.3339,5956.5,7.72471 +1953405,0.536922,-0.376858,-1.46589,-5.1975,13.3,6.1775,0.31472,0.29554,-0.39816,41.4878,-89.5062,184.687,0,0,33.3,1024.69,-101.346,-0.740234,-0.144531,-3.43164,6,6,6,6,0,1255.38,377.19,-44.3339,5956.5,7.72471 +1953415,-0.233264,-0.129503,-1.98262,-5.43375,9.135,1.79375,0.19852,0.3563,-0.3612,41.4878,-89.5062,184.687,0,0,33.3,1049.71,-317.613,-0.724609,-0.162109,-3.46875,6,6,6,6,0,1255.38,377.19,-44.3339,5956.5,7.60869 \ No newline at end of file diff --git a/Central-Server/API/services/serial_tester.py b/Central-Server/API/services/serial_tester.py index af8aed8..755c591 100644 --- a/Central-Server/API/services/serial_tester.py +++ b/Central-Server/API/services/serial_tester.py @@ -44,6 +44,8 @@ def TRY_WRITE(port: serial.Serial, data, component): FAIL("Write:" + port.name, "Timeout while writing data (" + component + ")") def RESET_TEST(port: serial.Serial): + if(port.in_waiting): + port.read_all() port.write("!test_reset".encode()) def FAIL(component, reason): @@ -85,8 +87,8 @@ def TEST(component, condition): FAIL(component, condition[1]) -def AWAIT_ANY_RESPONSE(port: serial.Serial, timeout=3.0): - wait_text("COM:" + port.name, "Awaiting a response from port..") +def AWAIT_ANY_RESPONSE(port: serial.Serial, timeout=3.0, comment=""): + wait_text("COM:" + port.name, "Awaiting a response from port.. " + comment) try: start_time = time.time() while(time.time() - start_time < timeout): @@ -111,7 +113,7 @@ def ENSURE_NO_RESPONSE(port: serial.Serial, timeout=3.0): def TRY_OPEN_PORT(port_name): wait_text(port_name, "Attempting to open port") try: - port = serial.Serial(port_name, timeout=5, write_timeout=2) + port = serial.Serial(port_name, timeout=5, write_timeout=0) PASS("COMPORT " + port_name, "Successfully connected to COM:" + port_name) return port except: @@ -124,8 +126,10 @@ def GET_PACKET(port: serial.Serial, timeout=3.0): start_time = time.time() while(time.time() - start_time < timeout): if port.in_waiting: - data = port.read_until(b"\n") + data = port.read_all() string = data.decode("utf8") + while port.in_waiting: + string += port.read_all().decode("utf8") if string: valid, type, data = pkt.decode_packet(string) @@ -136,7 +140,8 @@ def GET_PACKET(port: serial.Serial, timeout=3.0): FAIL("Read:" + port.name, "Read timeout") except: - FAIL("Read:" + port.name, "VALID_FORMAT ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") + print(string) + FAIL("Read:" + port.name, "GET_PACKET ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") # Reads last packet and detects if it's of a valid format def VALID_PACKET(port: serial.Serial, packet_type, valid, type, data, timeout=3.0): diff --git a/Central-Server/API/util/client_packets.py b/Central-Server/API/util/client_packets.py index 4261f44..89e7169 100644 --- a/Central-Server/API/util/client_packets.py +++ b/Central-Server/API/util/client_packets.py @@ -51,6 +51,15 @@ def construct_job_status(job_ok, current_action, status_text): #### SERVER PACKETS #### def decode_packet(packet: str): + if "[raw==>]" in packet: + # This is a job packet, we treat it differently. + packet_split = packet.split("[raw==>]") + job_packet = packet_split[0] + raw_data = packet_split[1] + packet_dict = json.loads(job_packet) + packet_dict['data']['csv_data'] = raw_data + return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] + packet_dict = json.loads(packet) return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] diff --git a/Central-Server/API/util/packets.py b/Central-Server/API/util/packets.py index 4d36134..61858cc 100644 --- a/Central-Server/API/util/packets.py +++ b/Central-Server/API/util/packets.py @@ -35,8 +35,8 @@ def construct_cycle(): def construct_job(job_data, flight_csv): # Constructs TERMINATE packet - packet_dict = {'type': "JOB", 'data': {'job_data': job_data, 'sim_data': flight_csv}} - return json.dumps(packet_dict) + packet_dict = {'type': "JOB", 'data': {'job_data': job_data}} + return json.dumps(packet_dict) + "[raw==>]" + flight_csv # [raw==>] used as delimiter #### CLIENT PACKETS #### def decode_packet(packet: str): diff --git a/Test-Rack-Software/TARS-Rack/av_platform/interface.py b/Test-Rack-Software/TARS-Rack/av_platform/interface.py index b78f7ef..65d5b7c 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/interface.py +++ b/Test-Rack-Software/TARS-Rack/av_platform/interface.py @@ -17,6 +17,7 @@ import time import serial import util.packets as packet +import traceback ready = False # Is the stack ready to recieve data? TARS_port: serial.Serial = None @@ -157,6 +158,11 @@ def __init__(self, raw_csv: str, job: dict) -> None: self.last_packet_time = self.start_time self.job_data = job + def reset_clock(self): + self.start_time = time.time() + self.current_time = self.start_time + self.last_packet_time = self.start_time + """ Runs one iteration of the HILSIM loop, with a change in time of dt. callback_func is a function to communicate back to the main process mid-step. @@ -165,8 +171,10 @@ def __init__(self, raw_csv: str, job: dict) -> None: """ def step(self, dt: float, callback_func): self.current_time += dt - if self.current_time > self.last_packet_time + 1: - self.last_packet_time += 0.01 + simulation_dt = 0.01 + if self.current_time > self.last_packet_time + simulation_dt: + self.last_packet_time += simulation_dt + if self.current_time < self.start_time + 5: # Wait for 5 seconds to make sure serial is connected pass diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index 1a29deb..c992c36 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -27,12 +27,19 @@ current_job_data: dict = None signal_abort = False job_active = False +job_clock_reset = False def ready(): return avionics.ready and server_port != None def setup_job(job_packet_data): global current_job + while(server_port.in_waiting): + data = server_port.read_all() + string = data.decode("utf8") + if string: + job_packet_data['csv_data'] += string + current_job = avionics.HilsimRun(job_packet_data['csv_data'], job_packet_data['job_data']) current_job_data = job_packet_data['job_data'] @@ -93,6 +100,7 @@ def cancel_job(): if(completed): job_active = True + job_clock_reset = False except Exception as e: log_string += "Job rejected: " + str(e) comport.write(packet.construct_invalid(packet_string).encode()) @@ -121,7 +129,7 @@ def read_data(listener_list: list[serial.Serial]): handle_server_packet(type, data, packet_string, comport) def main(): - global server_port, tester_boards, board_id, job_active + global server_port, tester_boards, board_id, job_active, job_clock_reset # TODO: Implement application-specific setup # TODO: Implement stack-specific setup # TODO: Implement Server-application communication (Serial communication) @@ -135,7 +143,7 @@ def main(): last_job_loop_time = time.time() print("(datastreamer) first setup done") - while True: # Run setup + while True: # Are we in init state? (Don't have a server connected) if server_port == None and time.time() > next_conn_debounce: print("(datastreamer) Missing server connection! Running server probe") @@ -169,16 +177,16 @@ def send_running_job_status(job_status_packet): # Runs currently active job (If any) if(job_active): + if(job_clock_reset == False): + current_job.reset_clock() + job_clock_reset = True + last_job_loop_time = time.time() + + if(last_job_loop_time != time.time()): dt = time.time() - last_job_loop_time run_finished, run_errored, cur_log = current_job.step(dt, send_running_job_status) - - # CURRENT ISSUE: - # Doesn't stream enough data. - - - if(run_finished): job_active = False pass diff --git a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py index 4fa4fba..4ffc2e8 100644 --- a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py +++ b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py @@ -4,6 +4,7 @@ import util.packets as packet connected_comports: list[serial.Serial] = [] +packet_buffer: dict = [] """ Function to retrieve a list of comports connected to this device @@ -92,4 +93,17 @@ def handle_server_packet(port: serial.Serial, raw_packet: str): def ping_ident(): for comport_info in get_com_ports(): comport = serial.Serial(comport_info.device, 9600, timeout=10) - send_ident(comport) \ No newline at end of file + send_ident(comport) + +"""Saves a packet to be sent next write_all() call""" +def write_to_buffer(port: serial.Serial, packet: str): + global packet_buffer + if(not port.name in packet_buffer.keys()): + packet_buffer[port.name] = [] + packet_buffer[port.name].append(packet.encode()) + +"""Write all packets in a port's packet buffer""" +def write_all(port: serial.Serial): + global packet_buffer + full_data = "[packet]".join(packet_buffer[port.name]) + port.write(full_data) \ No newline at end of file From b8fb5351bef9ee35efabb3363b402f643e026d0f Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 18 Oct 2023 11:46:59 -0500 Subject: [PATCH 09/24] More datastreamer work --- .../API/services/datastreamer_test.py | 80 +-- Central-Server/API/services/dummy_port.py | 9 + Central-Server/API/services/serial_tester.py | 80 ++- Central-Server/API/util/client_packets.py | 83 --- Central-Server/API/util/packets.py | 504 ++++++++++++++--- Test-Rack-Software/TARS-Rack/main.py | 158 +++++- Test-Rack-Software/TARS-Rack/sv_pkt.py | 66 --- .../TARS-Rack/test_server_delete.py | 28 - .../TARS-Rack/util/datastreamer_server.py | 139 +++++ .../TARS-Rack/util/handle_jobs.py | 6 + .../TARS-Rack/util/handle_packets.py | 41 ++ Test-Rack-Software/TARS-Rack/util/packets.py | 512 +++++++++++++++--- .../TARS-Rack/util/serial_wrapper.py | 56 +- 13 files changed, 1314 insertions(+), 448 deletions(-) create mode 100644 Central-Server/API/services/dummy_port.py delete mode 100644 Central-Server/API/util/client_packets.py delete mode 100644 Test-Rack-Software/TARS-Rack/sv_pkt.py delete mode 100644 Test-Rack-Software/TARS-Rack/test_server_delete.py create mode 100644 Test-Rack-Software/TARS-Rack/util/datastreamer_server.py create mode 100644 Test-Rack-Software/TARS-Rack/util/handle_jobs.py create mode 100644 Test-Rack-Software/TARS-Rack/util/handle_packets.py diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index 8b95783..9033e1b 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -8,6 +8,7 @@ import os import time import json +import util.packets as pkt sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) @@ -28,17 +29,20 @@ # > ACK 1 ack_test_boardid = 4 serial_tester.SECTION("[1/2] ACK - Valid Acknowledge packet") - serial_tester.RESET_TEST(port) - serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing valid ACK packet") + serial_tester.CLEAR(port) + + serial_tester.TRY_WRITE(port, pkt.SV_ACKNOWLEDGE(ack_test_boardid), "Writing valid ACK packet") + print(port.out_waiting) serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after ACK packet") - serial_tester.TEST("Responds to IDENT? packet after ACK packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("IDENT? packet after ACK packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + serial_tester.TRY_WRITE(port, pkt.SV_IDENT_PROBE(), "Writing IDENT? packet after ACK packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.ID_CONFIRM) + + serial_tester.TEST("IDENT? packet after ACK packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.ID_CONFIRM)) cond = False - res = "ID-CONF does not return correct board ID after ACK (Expected " + str(ack_test_boardid) + ", but got " + str(data['board_id']) + ")" - if(data['board_id'] == ack_test_boardid): + print(packet.data) + res = "ID-CONF does not return correct board ID after ACK (Expected " + str(ack_test_boardid) + ", but got " + str(packet.data['board_id']) + ")" + if(packet.data['board_id'] == ack_test_boardid): cond = True res = "ID-CONF correctly returned board ID " + str(ack_test_boardid) serial_tester.TEST("Connected board returns properly set ID", (cond, res)) @@ -47,38 +51,35 @@ # > PING serial_tester.SECTION("PING - Signal testing") - serial_tester.TRY_WRITE(port, pkt.construct_ping().encode(), "Writing PING packet") - serial_tester.TEST("Responds to PING packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, "PONG", valid, type, data)) + serial_tester.TRY_WRITE(port, pkt.SV_PING(), "Writing PING packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.PONG) + serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.PONG)) # > IDENT? serial_tester.SECTION("IDENT? - Identity confirmation") - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet") - serial_tester.TEST("Responds to IDENT? packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + serial_tester.TRY_WRITE(port, pkt.SV_IDENT_PROBE(), "Writing IDENT_PROBE packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.ID_CONFIRM) + serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.ID_CONFIRM)) # > ACK 2 (invalid) ack_test_boardid = 0 serial_tester.SECTION("[2/2] ACK - Acknowledge packet after ACK") - serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing invalid ACK packet") - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("INVALID packet after second ACK packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) + serial_tester.TRY_WRITE(port, pkt.SV_ACKNOWLEDGE(ack_test_boardid), "Writing invalid ACK packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.INVALID) + serial_tester.TEST("INVALID packet after second ACK packet", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.INVALID)) # > REASSIGN 1 reassign_test_boardid = 2 serial_tester.SECTION("REASSIGN - [Valid] Assign new board ID to rack") - serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing REASSIGN packet") + serial_tester.TRY_WRITE(port, pkt.SV_REASSIGN(reassign_test_boardid), "Writing REASSIGN packet") serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after REASSIGN packet") - serial_tester.TEST("Responds to IDENT? packet after REASSIGN packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("IDENT? packet after REASSIGN packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) + serial_tester.TRY_WRITE(port, pkt.SV_IDENT_PROBE(), "Writing IDENT? packet after REASSIGN packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.ID_CONFIRM) + serial_tester.TEST("IDENT? packet after REASSIGN packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.ID_CONFIRM)) cond = False - res = "ID-CONF does not return correct board ID after REASSIGN (Expected " + str(reassign_test_boardid) + ", but got " + str(data['board_id']) + ")" - if(data['board_id'] == reassign_test_boardid): + res = "ID-CONF does not return correct board ID after REASSIGN (Expected " + str(reassign_test_boardid) + ", but got " + str(packet.data['board_id']) + ")" + if(packet.data['board_id'] == reassign_test_boardid): cond = True res = "ID-CONF correctly returned board ID " + str(reassign_test_boardid) serial_tester.TEST("Connected board returns properly set ID", (cond, res)) @@ -88,18 +89,15 @@ serial_tester.SECTION("Initializes job and sends updates") # Get job data - - job_data = {"pull_type": "branch", "pull_target": "master", "job_type": "default", - "job_author_id": "github_id_here", "priority": 0, "timestep": 1} + job_data = pkt.JobData(0) # Open csv file file = open(os.path.join(os.path.dirname(__file__), "./datastreamer_test_data.csv"), 'r') csv_data = file.read() - serial_tester.TRY_WRITE(port, pkt.construct_job(job_data, csv_data).encode(), "Writing JOB packet") - serial_tester.TEST("Processes valid JOB packet and responds.. (extended time)", serial_tester.AWAIT_ANY_RESPONSE(port, 10)) - - valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TRY_WRITE(port, pkt.SV_JOB(job_data, csv_data), "Writing JOB packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.JOB_UPDATE, 30) + serial_tester.TEST("Packet after valid JOB packet complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) job_good = data['job_status']['job_ok'] == True and data['job_status']['status'] == "Accepted" serial_tester.TEST("Ensure job_ok is True and job_status is 'Accepted'", (job_good, f"Got job_ok {data['job_status']['job_ok']} and job_status '{data['job_status']['status']}'")) @@ -119,19 +117,29 @@ serial_tester.TEST("Ensure job_ok is True", (job_good, f"Got job_ok {data['job_status']['job_ok']}")) # Terminate + serial_tester.SECTION("Terminates gracefully and successfully") time.sleep(0.5) + serial_tester.CLEAR(port) serial_tester.TRY_WRITE(port, pkt.construct_terminate().encode(), "Writing TERMINATE packet") serial_tester.TEST("TERMINATE response sent (job packet)", serial_tester.AWAIT_ANY_RESPONSE(port, 3, "(Waiting for job update)")) valid, type, data = serial_tester.GET_PACKET(port) - - print(valid,type,data) + serial_tester.TEST("Packet after expected run force stop complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) + job_good = data['job_status']['job_ok'] == False + job_end_correct_reason = data['job_status']['current_action'] == "Stopped" + serial_tester.TEST("Ensure job_ok is False", (job_good, f"Got job_ok {data['job_status']['job_ok']}")) + serial_tester.TEST("Ensure current_action is 'Stopped' (Non-error stop code)", (job_end_correct_reason, f"Got current_action {data['job_status']['current_action']}")) + + # Next packet READY? + serial_tester.TEST("Await a READY packet", serial_tester.AWAIT_ANY_RESPONSE(port, 3)) + valid, type, data = serial_tester.GET_PACKET(port) + serial_tester.TEST("Packet after expected run force stop complies to READY", serial_tester.VALID_PACKET(port, "READY", valid, type, data)) # CLEANUP serial_tester.SECTION("Cleanup") serial_tester.TEST("Ensure empty serial bus", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) - serial_tester.RESET_TEST(port) + serial_tester.CLEAR(port) serial_tester.DONE() diff --git a/Central-Server/API/services/dummy_port.py b/Central-Server/API/services/dummy_port.py new file mode 100644 index 0000000..598bcc3 --- /dev/null +++ b/Central-Server/API/services/dummy_port.py @@ -0,0 +1,9 @@ +import serial +import json + +port = serial.Serial("COM9", timeout=5, write_timeout=0) + +print(json.loads('{"packet_type": 4, "packet_data": {}, "use_raw": true}')) + +while True: + pass \ No newline at end of file diff --git a/Central-Server/API/services/serial_tester.py b/Central-Server/API/services/serial_tester.py index 755c591..d9cdcb3 100644 --- a/Central-Server/API/services/serial_tester.py +++ b/Central-Server/API/services/serial_tester.py @@ -10,7 +10,6 @@ os.path.join(os.path.dirname(__file__), '..'))) import util.packets as pkt -import util.client_packets as cl_pkt success = 0 fails = 0 @@ -35,10 +34,10 @@ def display(result): print("ALL TESTS \033[92mSUCCESSFUL\033[0m") print("Test breakdown: \033[92m" + str(success) + " passed\033[0m\n") -def TRY_WRITE(port: serial.Serial, data, component): +def TRY_WRITE(port: serial.Serial, data: pkt.DataPacket, component): wait_text(port.name, "Attempting to write " + str(len(data)) + " bytes (" + component + ")") try: - port.write(data) + port.write(data.serialize().encode()) PASS("Write:" + port.name, component) except serial.SerialTimeoutException as exc: FAIL("Write:" + port.name, "Timeout while writing data (" + component + ")") @@ -48,6 +47,9 @@ def RESET_TEST(port: serial.Serial): port.read_all() port.write("!test_reset".encode()) +def CLEAR(port: serial.Serial): + port.reset_input_buffer() + def FAIL(component, reason): global fails, cur_wait_text print("\033[91mX\033[0m" + cur_wait_text.replace("#", "")) @@ -119,42 +121,66 @@ def TRY_OPEN_PORT(port_name): except: FAIL("COMPORT " + port_name, "Unable to open port:\033[90m\n\n" + traceback.format_exc() + "\033[0m") -def GET_PACKET(port: serial.Serial, timeout=3.0): - wait_text("Read:" + port.name, "Decoding Serial Packet") +def GET_PACKETS(port: serial.Serial, ignore_heartbeat=True, silent=False) -> list[pkt.DataPacket] | None: + if not silent: + wait_text("Read:" + port.name, "Decoding Serial Packet") + try: + list = pkt.DataPacketBuffer.serial_to_packet_list(port) + new_list = [] + if ignore_heartbeat: + for packet in list: + if(packet.packet_type != pkt.DataPacketType.HEARTBEAT): + new_list.append(packet) + else: + new_list = list + + + return new_list + except: + FAIL("Read:" + port.name, "GET_PACKET ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") +def WAIT_FOR_PACKET_TYPE(port: serial.Serial, packet_type: pkt.DataPacketType, timeout=3.0): + wait_text(port.name, "Waiting for packet of type " + str(packet_type)) try: start_time = time.time() while(time.time() - start_time < timeout): - if port.in_waiting: - data = port.read_all() - string = data.decode("utf8") - while port.in_waiting: - string += port.read_all().decode("utf8") - - if string: - valid, type, data = pkt.decode_packet(string) - if(valid): - return valid, type, data - else: - FAIL("Read:" + port.name, "Failed to decode packet") - - FAIL("Read:" + port.name, "Read timeout") + all_packets = GET_PACKETS(port, False, True) + for packet in all_packets: + if(packet.packet_type == packet_type): + return packet + FAIL("Wait for packet " + str(packet_type), "Timed out") + except: + FAIL("Wait for packet " + str(packet_type), "Encountered a non-recoverable error") + +def GET_PACKET(port: serial.Serial, ignore_heartbeat=True, timeout=3.0) -> pkt.DataPacket | None: + wait_text("Read:" + port.name, "Decoding Serial Packet") + try: + list = pkt.DataPacketBuffer.serial_to_packet_list(port) + new_list = [] + if ignore_heartbeat: + for packet in list: + if(packet.packet_type != pkt.DataPacketType.HEARTBEAT): + new_list.append(packet) + else: + new_list = list + + return new_list[0] except: - print(string) FAIL("Read:" + port.name, "GET_PACKET ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") + # Reads last packet and detects if it's of a valid format -def VALID_PACKET(port: serial.Serial, packet_type, valid, type, data, timeout=3.0): - wait_text("Check packet format", "Checking validity of " + packet_type + " packet") +def VALID_PACKET(port: serial.Serial, packet: pkt.DataPacket, packet_type: pkt.DataPacketType, timeout=3.0): + wait_text("Check packet format", "Checking validity of " + str(packet_type) + " packet") try: - if(type == packet_type): - if(valid): - return True, "Packet complies to type " + packet_type + if(packet.packet_type == packet_type): + if(pkt.PacketValidator.is_client_packet(packet) and pkt.PacketValidator.validate_client_packet): + return True, "Packet complies to type " + str(packet_type) else: - return False, "VALID_FORMAT FAILED for " + packet_type + ", got \033[90m\n\n" + type + ": " + data + "\033[0m" + return False, "VALID_FORMAT FAILED for " + str(packet_type) + ", got \033[90m\n\n" + str(packet.packet_type) + ": " + str(packet) + "\033[0m" else: - return False, "VALID_FORMAT recieved a different packet type than " + packet_type + return False, "VALID_FORMAT recieved a different packet type than " + str(packet_type) except: return False, "VALID_FORMAT ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m" \ No newline at end of file diff --git a/Central-Server/API/util/client_packets.py b/Central-Server/API/util/client_packets.py deleted file mode 100644 index 89e7169..0000000 --- a/Central-Server/API/util/client_packets.py +++ /dev/null @@ -1,83 +0,0 @@ -# Contains all communication packets required for server-datastreamer communication. -# Contains constructor functions for each packet and also a decode function for server packets. - -import json - -#### CLIENT PACKETS #### -def construct_ident(board_type: str): - # Constructs IDENT packet - packet_dict = {'type': "IDENT", 'data': {'board_type': board_type}} - return json.dumps(packet_dict) - -def construct_id_confirm(board_type: str, board_id: int): - # Constructs ID-CONF packet - packet_dict = {'type': "ID-CONF", 'data': {'board_type': board_type, 'board_id': board_id}} - return json.dumps(packet_dict) - -def construct_ready(): - # Constructs READY packet - packet_dict = {'type': "READY", 'data': {}} - return json.dumps(packet_dict) - -def construct_done(job_data, hilsim_result: str): - # Constructs DONE packet - packet_dict = {'type': "DONE", 'data': {'job_data': job_data, 'hilsim_result': hilsim_result}} - return json.dumps(packet_dict) - -def construct_invalid(raw_packet): - # Constructs INVALID packet - packet_dict = {'type': "INVALID", 'data': {'raw_packet': raw_packet}} - return json.dumps(packet_dict) - -def construct_busy(job_data): - # Constructs BUSY packet - packet_dict = {'type': "BUSY", 'data': {'job_data': job_data}} - return json.dumps(packet_dict) - -def construct_job_update(job_status, current_log: str): - # Constructs JOB-UPD packet - packet_dict = {'type': "JOB-UPD", 'data': {'job_status': job_status, 'hilsim_result': current_log}} - return json.dumps(packet_dict) - -def construct_pong(): - # Constructs PONG packet - packet_dict = {'type': "PONG", 'data': {}} - return json.dumps(packet_dict) - - -#### Intermediate data #### -def construct_job_status(job_ok, current_action, status_text): - return {"job_ok": job_ok, 'current_action': current_action, "status": status_text} - -#### SERVER PACKETS #### -def decode_packet(packet: str): - if "[raw==>]" in packet: - # This is a job packet, we treat it differently. - packet_split = packet.split("[raw==>]") - job_packet = packet_split[0] - raw_data = packet_split[1] - packet_dict = json.loads(job_packet) - packet_dict['data']['csv_data'] = raw_data - return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] - - packet_dict = json.loads(packet) - return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] - -def validate_server_packet(packet_type: str, packet_data): - match packet_type: - case "IDENT?": - return True - case "ACK": - return "board_id" in packet_data - case "REASSIGN": - return "board_id" in packet_data - case "TERMINATE": - return True - case "CYCLE": - return True - case "JOB": - return "job_data" in packet_data and "csv_data" in packet_data - case "PONG": - return True - - \ No newline at end of file diff --git a/Central-Server/API/util/packets.py b/Central-Server/API/util/packets.py index 61858cc..e625178 100644 --- a/Central-Server/API/util/packets.py +++ b/Central-Server/API/util/packets.py @@ -1,68 +1,444 @@ -# Contains all communication packets required for the SERVER side of the server-datastreamer communication. -# Contains constructor functions for each packet and also a decode function for client packets. +# Contains all communication packets required for server-datastreamer communication. +# Contains constructor functions for each packet and also a decode function for server packets. + import json +from enum import Enum +import serial + +class PacketDeserializeException(Exception): + "Raised a packet fails to deserialize" + def __init__(self, packet_string, reason="Generic deserialization error", message="Failed to deserialize packet"): + self.packet_string = packet_string + self.message = message + self.reason = reason + if len(self.packet_string) > 100: + self.packet_string = self.packet_string[0:99] + ".. (rest hidden)" + super().__init__(self.message + " " + packet_string + " ==> " + self.reason) + +class DataPacketType(int, Enum): + IDENT = 0 + ID_CONFIRM = 1 + READY = 2 + DONE = 3 + INVALID = 4 + BUSY = 5 + JOB_UPDATE = 6 + PONG = 7 + HEARTBEAT = 8 + # Server packets + IDENT_PROBE = 100 + PING = 101 + ACKNOWLEDGE = 102 + REASSIGN = 103 + TERMINATE = 104 + CYCLE = 105 + JOB = 106 + # Misc packets + RAW = 200 + +class DataPacket: + """ + A class that stores the relevant data for any type of data packet + """ + packet_type: str = "RAW" + data: dict = None + use_raw: bool = False + raw_data: str = "" + + def __init__(self, p_type:DataPacketType, p_data:dict, data_raw=None) -> None: + """ + Initializes the data packet. With raw data capabilities if raw_data is passed in. + @p_type: The type of packet + @p_data: Data stored within the packet + @data_raw: The raw string to be decoded + """ + self.packet_type = p_type + self.data = p_data + if data_raw != None: + self.use_raw = True + self.raw_data = data_raw + + def safe_deserialize(self, packet_string: str) -> None: + """ + Deserializes a packet, running through possibilities of recoverable errors if an error is encountered. + **CAREFUL!** This is an expensive operation, and should only be used if you are not sure if the stream is clear. + """ + offset = 0 + + while(offset < len(packet_string)): + try: + self.deserialize(packet_string[offset:]) + return + except: + offset += 1 + + + def deserialize(self, packet_string: str) -> None: + """ + Deserialization constructor for DataPacket + @packet_string: Raw packet string to deserialize + """ + packet_header = None + if "[[raw==>]]" in packet_string: + # If the packet uses raw: + self.use_raw = True + p_split = packet_string.split("[[raw==>]]") # Element 0 will be proper packet json, element 1 will be raw data + try: + self.data = json.loads(p_split[0])['packet_data'] + packet_header = json.loads(p_split[0]) + except: + raise PacketDeserializeException(packet_string, "Unable to deserialize packet header JSON") + self.raw_data = p_split[1] + + else: + # If the packet does not use raw: + self.use_raw = False + try: + self.data = json.loads(packet_string)['packet_data'] + packet_header = json.loads(packet_string) + except Exception as e: + raise PacketDeserializeException(packet_string, "Unable to deserialize packet header JSON") + + # Set proper enum value + if "packet_type" in packet_header: + self.packet_type = DataPacketType(packet_header['packet_type']) + else: + raise PacketDeserializeException(packet_string, "Unable to access packet_type key in packet data") + def serialize(self) -> str: + """ + Serializes one packet to a raw data string + """ + packet_dict = {'packet_type': self.packet_type, 'packet_data': self.data, 'use_raw': self.use_raw} + string_construct = json.dumps(packet_dict) + if(self.use_raw): + string_construct += "[[raw==>]]" + self.raw_data + return string_construct + + def __len__(self) -> int: + return len(self.serialize()) + + def __str__(self) -> str: + if self.use_raw: + if(len(self.raw_data) > 50): + return " " + str(self.data) + " [raw: +" + str(len(self.raw_data)) + "c]" + else: + return " " + str(self.data) + " [raw: " + self.raw_data + "]" + else: + return " " + str(self.data) + +class DataPacketBuffer: + """ + A utility class for encoding and decoding data streams with packets. + """ + packet_buffer: list[DataPacket] = [] + input_buffer: list[DataPacket] = [] + + def __init__(self, packet_list:list[DataPacket]=[]) -> None: + self.packet_buffer = packet_list + + def add(self, packet: DataPacket) -> None: + """Adds a packet to the packet buffer""" + self.packet_buffer.append(packet) + + def write_buffer_to_serial(self, serial_port: serial.Serial) -> None: + """ + Writes the entire packet buffer to serial + """ + serialized_full: str = self.to_serialized_string() + self.packet_buffer = [] + serial_port.write(serialized_full.encode()) + + + def write_packet(packet: DataPacket, serial_port: serial.Serial) -> None: + """ + Writes a single packet to serial + WARNING: using `write_packet` twice in a row will cause a deserialization error to be thrown due to a lack of a delimeter. + """ + serial_port.write(packet.serialize().encode()) + + def to_serialized_string(self) -> None: + """ + Serializes the packet buffer + """ + serialized_packets: list[str] = [] + for packet in self.packet_buffer: + serialized_packets.append(packet.serialize()) + serialized_full: str = "[[pkt_end]]".join(serialized_packets) + return serialized_full + + def stream_to_packet_list(stream: str, safe_deserialize:bool=False) -> list[DataPacket]: + """ + Converts serialized packet buffer to a list of packets + """ + serialized_packets = stream.split("[[pkt_end]]") + ds_packets: list[DataPacket] = [] + for ser_pkt in serialized_packets: + new_packet = DataPacket(DataPacketType.RAW, {}) + if(safe_deserialize): + new_packet.safe_deserialize(ser_pkt) + else: + new_packet.deserialize(ser_pkt) + + ds_packets.append(new_packet) + return ds_packets + + def read_to_input_buffer(self, port: serial.Serial) -> None: + """ + Appends all current packets in the port's buffer into the DataPacketBuffer input buffer. + """ + in_buffer = DataPacketBuffer.serial_to_packet_list(port) + for in_packet in in_buffer: + self.input_buffer.append(in_packet) + + def serial_to_packet_list(port: serial.Serial, safe_deserialize:bool=False) -> list[DataPacket]: + """ + Converts all packets in a serial port's input buffer into a list of packets. + """ + if(port.in_waiting): + instr = "" + while(port.in_waiting): + data = port.read_all() + string = data.decode("utf8") + if string: + instr += string + return DataPacketBuffer.stream_to_packet_list(instr, safe_deserialize) + return [] + + def clear_input_buffer(self): + """ + Clears this input buffer + """ + self.input_buffer = [] + + + +class JobData: + class GitPullType(int, Enum): + BRANCH = 0 # Pull a branch + COMMIT = 1 # Pull a specific commit + + class JobType(int, Enum): + DEFAULT = 0 # Pull code, flash, run hilsim, return result + DIRTY = 1 # Run hilsim with whatever is currently flashed, return result (tbd) + TEST = 2 # Run some sort of test suite (tbd) + + class JobPriority(int, Enum): + NORMAL = 0 # Normal priority, goes through queue like usual + HIGH = 1 # High priority, get priority in the queue + + + job_id:int + pull_type: GitPullType = GitPullType.BRANCH # Whether to pull from a branch or a specific commit (or other target) + pull_target: str = "master" # Which commit id / branch to pull from + job_type: JobType = JobType.DEFAULT + job_author_id: str = "" # We won't allow anonymous jobs, and we don't know what the id will look like yet, so this is a placeholder + job_priority: JobPriority = JobPriority.NORMAL # Only 2 states for now, but we can add more later if we come up with something else + job_timestep: int = 1 # How "precise" the job should be. 1 is the highest, higher numbers mean that some data points will be skipped but the job runs faster + + def __init__(self, job_id, pull_type: GitPullType=GitPullType.BRANCH, pull_target:str="master", + job_type:JobType=JobType.DEFAULT, job_author_id:str="", job_priority:JobPriority=JobPriority.NORMAL, + job_timestep:int=1) -> None: + self.job_id = job_id + self.pull_type = pull_type + self.pull_target = pull_target + self.job_type = job_type + self.job_author_id = job_author_id + self.job_priority = job_priority + self.job_timestep = job_timestep + + def to_dict(self) -> dict: + return {'job_id': self.job_id, 'pull_type': self.pull_type, 'pull_target': self.pull_target, + 'job_type': self.job_type, 'job_author_id': self.job_author_id, 'job_priority': self.job_priority, + 'job_timestep': self.job_timestep} -#### SERVER PACKETS #### -def construct_ident_probe(): - # Constructs IDENT? packet - packet_dict = {'type': "IDENT?", 'data': {}} - return json.dumps(packet_dict) - -def construct_ping(): - # Constructs PING packet - packet_dict = {'type': "PING", 'data': {}} - return json.dumps(packet_dict) - -def construct_acknowledge(board_id: int): - # Constructs ACK packet - packet_dict = {'type': "ACK", 'data': {'board_id': board_id}} - return json.dumps(packet_dict) - -def construct_reassign(board_id: int): - # Constructs ACK packet - packet_dict = {'type': "REASSIGN", 'data': {'board_id': board_id}} - return json.dumps(packet_dict) - -def construct_terminate(): - # Constructs TERMINATE packet - packet_dict = {'type': "TERMINATE", 'data': {}} - return json.dumps(packet_dict) - -def construct_cycle(): - # Constructs TERMINATE packet - packet_dict = {'type': "CYCLE", 'data': {}} - return json.dumps(packet_dict) - -def construct_job(job_data, flight_csv): - # Constructs TERMINATE packet - packet_dict = {'type': "JOB", 'data': {'job_data': job_data}} - return json.dumps(packet_dict) + "[raw==>]" + flight_csv # [raw==>] used as delimiter + + +class JobStatus: + """Static class for making job statuses""" + class JobState(int, Enum): + IDLE = 0 + ERROR = 1 + SETUP = 2 + RUNNING = 3 + + job_state: JobState = JobState.IDLE + current_action: str = "" + status_text: str = "" + def __init__(self, job_state: JobState, current_action:str, status_text:str) -> None: + self.job_state = job_state + self.current_action = current_action + self.status_text = status_text + + def to_dict(self) -> dict: + return {'job_state': self.job_state, 'current_action': self.current_action, 'status_text': self.status_text} + +class HeartbeatServerStatus: + server_state: Enum # ServerState, from main. + server_startup_time: float # (Time.time()) + is_busy: bool # Current job running, being set up, or in cleanup. + is_ready: bool # Ready for another job + + def __init__(self, server_state: Enum, server_startup_time: float, + is_busy: bool, is_ready: bool) -> None: + self.server_state = server_state + self.server_startup_time = server_startup_time + self.is_busy = is_busy + self.is_ready = is_ready + + def to_dict(self) -> dict: + return {'server_state': self.server_state, 'server_startup_time': self.server_startup_time, + 'is_busy': self.is_busy, 'is_ready': self.is_ready} + + +class HeartbeatAvionicsStatus: + connected:bool # Connected to server + avionics_type:str # May turn this into an enum + # More debug info to come + + def __init__(self, connected:bool, avionics_type:str) -> None: + self.connected = connected + self.avionics_type = avionics_type + + def to_dict(self): + return {'connected': self.connected, 'avionics_type':self.avionics_type} + #### CLIENT PACKETS #### -def decode_packet(packet: str): - packet_dict = json.loads(packet) - if(type(packet_dict) == str): - packet_dict = json.loads(packet_dict) - return validate_client_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] - -def validate_client_packet(packet_type: str, packet_data): - match packet_type: - case "IDENT": - return "board_type" in packet_data - case "ID-CONF": - return "board_id" in packet_data and "board_type" in packet_data - case "READY": - return True - case "DONE": - return "hilsim_result" in packet_data and "job_data" in packet_data - case "JOB-UPD": - return "hilsim_result" in packet_data and "job_status" in packet_data - case "INVALID": - return "raw_packet" in packet_data - case "BUSY": - return "job_data" in packet_data - case "PONG": - return True - - - \ No newline at end of file +# Client packets are prefixed with CL +def CL_IDENT(board_type: str) -> DataPacket: + """Constructs IDENT packet + @board_type: The type of avionics stack for this packet""" + packet_data = {'board_type': board_type} + return DataPacket(DataPacketType.IDENT, packet_data) + +def CL_ID_CONFIRM(board_type: str, board_id: int) -> DataPacket: + """Constructs ID_CONFIRM packet + @board_type: The type of avionics stack connected to this server + @board_id: The ID assigned to this board before server restart.""" + packet_data = {'board_type': board_type, 'board_id': board_id} + return DataPacket(DataPacketType.ID_CONFIRM, packet_data) + +def CL_READY() -> DataPacket: + """Constructs READY packet""" + packet_data = {} + return DataPacket(DataPacketType.READY, packet_data) + +def CL_DONE(job_data: JobData, hilsim_result:str) -> DataPacket: + """Constructs DONE packet (RAW) + @job_data: The job data sent along with this packet (For ID purposes) + @hilsim_result: Raw string of the HILSIM output""" + packet_data = {'job_data': job_data.to_dict()} + return DataPacket(DataPacketType.READY, packet_data, hilsim_result) + +def CL_INVALID(invalid_packet:DataPacket) -> DataPacket: + """Constructs INVALID packet (RAW) + @raw_packet: The packet that triggered the INVALID response""" + packet_data = {} + return DataPacket(DataPacketType.INVALID, packet_data, str(invalid_packet)) + +def CL_BUSY(job_data: JobData) -> DataPacket: + """Constructs BUSY packet + @job_data: Job data for current job""" + packet_data = {'job_data': job_data.to_dict()} + return DataPacket(DataPacketType.BUSY, packet_data) + +def CL_JOB_UPDATE(job_status: JobStatus, current_log: str) -> DataPacket: + """Constructs JOB_UPDATE packet (RAW) + @job_status: State of current job + @current_log: Current outputs of HILSIM + """ + packet_data = {'job_status': job_status.to_dict()} + return DataPacket(DataPacketType.BUSY, packet_data, current_log) + +def CL_PONG() -> DataPacket: + """Constructs PONG packet""" + packet_data = {} + return DataPacket(DataPacketType.PONG, packet_data) + +def CL_HEARTBEAT(server_status: HeartbeatServerStatus, av_status: HeartbeatAvionicsStatus) -> DataPacket: + """Constructs HEARTBEAT packet + @server_status: State of the serve + @av_status: Connection and working status of avionics. + """ + packet_data = {'server_status': server_status.to_dict(), "avionics_status": av_status.to_dict()} + return DataPacket(DataPacketType.HEARTBEAT, packet_data) + +#### SERVER PACKETS #### +# Server packets are prefixed with SV +def SV_IDENT_PROBE() -> DataPacket: + """Constructs IDENT_PROBE packet""" + packet_data = {} + return DataPacket(DataPacketType.IDENT_PROBE, packet_data) + +def SV_PING() -> DataPacket: + """Constructs PING packet""" + packet_data = {} + return DataPacket(DataPacketType.PING, packet_data) + +def SV_ACKNOWLEDGE(board_id: int) -> DataPacket: + """Constructs IDENT_PROBE packet""" + packet_data = {'board_id': board_id} + return DataPacket(DataPacketType.ACKNOWLEDGE, packet_data) + +def SV_REASSIGN(board_id: int) -> DataPacket: + """Constructs REASSIGN packet""" + packet_data = {'board_id': board_id} + return DataPacket(DataPacketType.REASSIGN, packet_data) + +def SV_TERMINATE() -> DataPacket: + """Constructs TERMINATE packet""" + packet_data = {} + return DataPacket(DataPacketType.TERMINATE, packet_data) + +def SV_CYCLE() -> DataPacket: + """Constructs CYCLE packet""" + packet_data = {} + return DataPacket(DataPacketType.CYCLE, packet_data) + +def SV_JOB(job_data: JobData, flight_csv: str) -> DataPacket: + """Constructs JOB packet""" + packet_data = {'job_data': job_data.to_dict()} + return DataPacket(DataPacketType.JOB, packet_data, flight_csv) + +class PacketValidator: + def is_server_packet(packet: DataPacket): + return packet.packet_type.value > 99 and packet.packet_type.value < 199 + + def is_client_packet(packet: DataPacket): + return packet.packet_type.value > -1 and packet.packet_type.value < 99 + + def validate_server_packet(server_packet: DataPacket): + match server_packet.packet_type: + case DataPacketType.IDENT_PROBE: + return True + case DataPacketType.ACKNOWLEDGE: + return "board_id" in server_packet.data + case DataPacketType.REASSIGN: + return "board_id" in server_packet.data + case DataPacketType.TERMINATE: + return True + case DataPacketType.CYCLE: + return True + case DataPacketType.JOB: + return "job_data" in server_packet.data and server_packet.use_raw + case DataPacketType.PING: + return True + + def validate_client_packet(client_packet: DataPacket): + match client_packet.packet_type: + case DataPacketType.IDENT: + return "board_type" in client_packet.data + case DataPacketType.ID_CONFIRM: + return "board_id" in client_packet.data and "board_type" in client_packet.data + case DataPacketType.READY: + return True + case DataPacketType.DONE: + return client_packet.use_raw and "job_data" in client_packet.data + case DataPacketType.JOB_UPDATE: + return client_packet.use_raw and "job_status" in client_packet.data + case DataPacketType.INVALID: + return client_packet.use_raw + case DataPacketType.BUSY: + return "job_data" in client_packet.data + case DataPacketType.PONG: + return True \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index c992c36..e097a10 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -11,26 +11,127 @@ import av_platform.interface as avionics import av_platform.stream_data as platform -import util.serial_wrapper as server +import util.serial_wrapper as connection import util.packets as packet import util.config as config import os import time import serial +from enum import Enum +import util.handle_packets as handle_packets +import util.datastreamer_server as Datastreamer ############### HILSIM Data-Streamer-RASPI ############### -server_port: serial.Serial = None -tester_boards: list[serial.Serial] = None -board_id = -1 -current_job: avionics.HilsimRun = None -current_job_data: dict = None -signal_abort = False -job_active = False -job_clock_reset = False +def handle_error_state(Server: Datastreamer.DatastreamerServer): + pass + +def handle_job_setup_error(Server: Datastreamer.DatastreamerServer): + pass + +def handle_job_runtime_error(Server: Datastreamer.DatastreamerServer): + pass + +def handle_first_setup(Server: Datastreamer.DatastreamerServer): + try: + Server.board_type = config.board_type + avionics.first_setup() + return True + except: + return False + +def should_heartbeat(Server: Datastreamer.DatastreamerServer): + if(time.time() > Server.next_heartbeat_time): + Server.next_heartbeat_time = time.time() + 3 # Heartbeat every 3 seconds + return True + return False + +# Server connection +def send_wide_ident(Server: Datastreamer.DatastreamerServer): + if(time.time() > Server.last_server_connection_check): + connection.t_init_com_ports() + for port in connection.connected_comports: + packet.DataPacketBuffer.write_packet(packet.CL_IDENT(config.board_type), port) + Server.last_server_connection_check += 0.25 + return True + + +def check_server_connection(Server: Datastreamer.DatastreamerServer): + global board_id, server_port + for port in connection.connected_comports: + in_packets = packet.DataPacketBuffer.serial_to_packet_list(port, True) + for pkt in in_packets: + if pkt.packet_type == packet.DataPacketType.ACKNOWLEDGE: + Server.board_id = pkt.data['board_id'] + Server.server_port = port + return True + return False + +def reset_connection_data(Server: Datastreamer.DatastreamerServer): + Server.last_server_connection_check = time.time() + Server.next_heartbeat_time = time.time() + 3 + connection.t_init_com_ports() + for port in connection.connected_comports: + connection.hard_reset(port) + return True + +def detect_avionics(Server: Datastreamer.DatastreamerServer): + # Let's assume we can detect avionics already + return True + +def main(): + Server = Datastreamer.instance + SState = Datastreamer.ServerStateController.ServerState # SState alias + + # Make sure setup is done before any transition: + Server.state.add_transition_event(SState.INIT, SState.ANY, handle_first_setup) # Require repo first_setup to transition from INIT + + # FSM error handling: + # Generic error + Server.state.add_transition_event(SState.ANY, SState.ERROR, handle_error_state) + Server.state.add_transition_event(SState.JOB_SETUP, SState.JOB_ERROR, handle_job_setup_error) + Server.state.add_transition_event(SState.JOB_RUNNING, SState.JOB_ERROR, handle_job_runtime_error) + + # Set up FSM flow: + Server.state.add_transition_pipe(SState.INIT, SState.CONNECTING) # Always try to transition INIT => CONNECTING + Server.state.add_transition_pipe(SState.CONNECTING, SState.AV_DETECT) # Always try to transition CONNECTING => AV_DETECT + Server.state.add_transition_pipe(SState.AV_DETECT, SState.READY) # Always try to transition AV_DETECT => READY + Server.state.add_transition_pipe(SState.JOB_SETUP, SState.JOB_RUNNING) # Always try to transition JOB_SETUP => JOB_RUNNING + Server.state.add_transition_pipe(SState.JOB_RUNNING, SState.CLEANUP) # Always try to transition JOB_RUNNING => CLEANUP + Server.state.add_transition_pipe(SState.CLEANUP, SState.READY) # Always try to transition CLEANUP => READY + Server.state.add_transition_pipe(SState.JOB_ERROR, SState.CLEANUP) # Always try to transition JOB_ERROR => CLEANUP (for graceful recovery) + + # Connection checks + Server.state.add_transition_event(SState.ANY, SState.CONNECTING, reset_connection_data) + Server.state.add_transition_event(SState.CONNECTING, SState.AV_DETECT, send_wide_ident) + Server.state.add_transition_event(SState.CONNECTING, SState.AV_DETECT, check_server_connection) + Server.state.add_transition_event(SState.AV_DETECT, SState.READY, detect_avionics) + + handle_packets.add_transitions(Server.state) + handle_packets.add_always_events(Server.state) -def ready(): - return avionics.ready and server_port != None + + while True: + if Server.server_port != None: + Server.packet_buffer.write_buffer_to_serial(Server.server_port) + Server.packet_buffer.read_to_input_buffer(Server.server_port) + + Server.state.update_transitions() # Run all transition tests + + # Actions that always happen: + if(Server.server_port != None and should_heartbeat(Server)): + print("(heartbeat) Sent update at u+", time.time()) + Server.packet_buffer.add(packet.CL_HEARTBEAT(packet.HeartbeatServerStatus(Server.state.server_state, Server.server_start_time, False, False), + packet.HeartbeatAvionicsStatus(False, ""))) + # End "Always" actions. + + Server.packet_buffer.clear_input_buffer() + + +def send_ready(): + global server_port + connection.write_to_buffer(server_port, packet.construct_ready()) + connection.write_all(server_port) def setup_job(job_packet_data): global current_job @@ -44,7 +145,7 @@ def setup_job(job_packet_data): current_job_data = job_packet_data['job_data'] def handle_server_packet(packet_type, packet_data, packet_string, comport: serial.Serial): - global server_port, job_active + global server_port, job_active, signal_abort if(packet_type != "JOB"): print("Handling packet << " + packet_string) else: @@ -61,10 +162,6 @@ def handle_server_packet(packet_type, packet_data, packet_string, comport: seria comport.write(packet.construct_invalid(packet_string).encode()) return - if (not ready()): - print("(datastreamer) Recieved packet but not ready! Send ACK first.") - comport.write(packet.construct_invalid(packet_string).encode()) - return log_string = "" match packet_type: case "IDENT?": @@ -101,9 +198,15 @@ def cancel_job(): if(completed): job_active = True job_clock_reset = False + elif (signal_abort): + send_ready() except Exception as e: log_string += "Job rejected: " + str(e) comport.write(packet.construct_invalid(packet_string).encode()) + case "TERMINATE": + # Terminate currently running job + signal_abort = True + if(log_string == ""): log_string = "(handle_packet) No special log string for pkt\n" + packet_string else: @@ -128,8 +231,8 @@ def read_data(listener_list: list[serial.Serial]): # Is server packet handle_server_packet(type, data, packet_string, comport) -def main(): - global server_port, tester_boards, board_id, job_active, job_clock_reset +def main_old(): + global server_port, board_id, job_active, job_clock_reset # TODO: Implement application-specific setup # TODO: Implement stack-specific setup # TODO: Implement Server-application communication (Serial communication) @@ -147,10 +250,9 @@ def main(): # Are we in init state? (Don't have a server connected) if server_port == None and time.time() > next_conn_debounce: print("(datastreamer) Missing server connection! Running server probe") - server.t_init_com_ports() - for port in server.connected_comports: - server.send_ident(port) - + connection.t_init_com_ports() + for port in connection.connected_comports: + connection.send_ident(port) next_conn_debounce = time.time() + 0.5 @@ -164,7 +266,7 @@ def main(): print("(datastreamer) Avionics ready!") next_av_debounce = time.time() + 0.1 - listener_list = server.connected_comports + listener_list = connection.connected_comports if(server_port != None): listener_list = [server_port] @@ -177,6 +279,16 @@ def send_running_job_status(job_status_packet): # Runs currently active job (If any) if(job_active): + if(signal_abort): + # Stop job immediately and notify server of status + job_active = False + job_status = packet.construct_job_status(False, "Stopped", "The job has been forcefully stopped") + print("(run_job) Job terminated") + + connection.write_to_buffer(server_port, packet.construct_job_update(job_status, current_job.get_current_log())) + send_ready() + continue + if(job_clock_reset == False): current_job.reset_clock() job_clock_reset = True diff --git a/Test-Rack-Software/TARS-Rack/sv_pkt.py b/Test-Rack-Software/TARS-Rack/sv_pkt.py deleted file mode 100644 index f680019..0000000 --- a/Test-Rack-Software/TARS-Rack/sv_pkt.py +++ /dev/null @@ -1,66 +0,0 @@ -# Contains all communication packets required for the SERVER side of the server-datastreamer communication. -# Contains constructor functions for each packet and also a decode function for client packets. -import json - -#### SERVER PACKETS #### -def construct_ident_probe(): - # Constructs IDENT? packet - packet_dict = {'type': "IDENT?", 'data': {}} - return json.dumps(packet_dict) - -def construct_ping(): - # Constructs PING packet - packet_dict = {'type': "PING", 'data': {}} - return json.dumps(packet_dict) - -def construct_acknowledge(board_id: int): - # Constructs ACK packet - packet_dict = {'type': "ACK", 'data': {'board_id': board_id}} - return json.dumps(packet_dict) - -def construct_reassign(board_id: int): - # Constructs ACK packet - packet_dict = {'type': "REASSIGN", 'data': {'board_id': board_id}} - return json.dumps(packet_dict) - -def construct_terminate(): - # Constructs TERMINATE packet - packet_dict = {'type': "TERMINATE", 'data': {}} - return json.dumps(packet_dict) - -def construct_cycle(): - # Constructs TERMINATE packet - packet_dict = {'type': "CYCLE", 'data': {}} - return json.dumps(packet_dict) - -def construct_job(job_data, flight_csv): - # Constructs TERMINATE packet - packet_dict = {'type': "JOB", 'data': {'job_data': job_data, 'sim_data': flight_csv}} - return json.dumps(packet_dict) - -#### CLIENT PACKETS #### -def decode_packet(packet: str) -> tuple[bool, str, dict]: - packet_dict = json.loads(packet) - return validate_client_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] - -def validate_client_packet(packet_type: str, packet_data): - match packet_type: - case "IDENT": - return "board_type" in packet_data - case "ID-CONF": - return "board_id" in packet_data and "board_type" in packet_data - case "READY": - return True - case "DONE": - return "hilsim_result" in packet_data and "job_data" in packet_data - case "JOB-UPD": - return "hilsim_result" in packet_data and "job_status" in packet_data - case "INVALID": - return "raw_packet" in packet_data - case "BUSY": - return "job_data" in packet_data - case "PONG": - return True - - - \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/test_server_delete.py b/Test-Rack-Software/TARS-Rack/test_server_delete.py deleted file mode 100644 index 75b44e6..0000000 --- a/Test-Rack-Software/TARS-Rack/test_server_delete.py +++ /dev/null @@ -1,28 +0,0 @@ -# DELETE THIS! - -import serial -import sys -import sv_pkt as packet -import time - -argc = len(sys.argv) - -comport = "COM9" -port = serial.Serial(comport, write_timeout=1.0) - -print("Init") -start = time.time() -delay_until_ack = 0.5; ack_run = False -delay_until_ident = 1; ident_run = False - -while True: - if port.in_waiting: - data = port.read_all() - packet_string = data.decode("utf8") - valid, p_type, data = packet.decode_packet(packet_string) - - print( type(data) ) - print(valid, p_type, data) - - - diff --git a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py b/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py new file mode 100644 index 0000000..10b6622 --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py @@ -0,0 +1,139 @@ +import sys +import os + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import av_platform.interface as avionics +import util.packets as packet +import time +import serial +from enum import Enum + +class ServerStateController(): + # A state machine for the server + server = None + class ServerState(int, Enum): + ANY = -1 + INIT = 0 + CONNECTING = 1 # Before server connection + AV_DETECT = 2 # Before detecting avionics + READY = 3 # Ready to recieve jobs + JOB_SETUP = 4 # Recieved a job, in setup + JOB_RUNNING = 5 # Running a job + CLEANUP = 6 # Finished job, cleaning up environment + ERROR = 100 # Error state (recoverable) + FERROR = 101 # Error state (unrecoverable) + JOB_ERROR = 102 # Error during a job + + class Always: + """A class representing an action that should always be run in a certain state""" + def __init__(self, server, target_state, callback) -> None: + self.server: DatastreamerServer = server + self.target_state: ServerStateController.ServerState = target_state + self.callback = callback + + def run(self, current_state): + if(self.target_state == current_state or self.target_state == ServerStateController.ServerState.ANY): + self.callback(self.server) + + class StateTransition: + """ + A class respresenting code that must be executed for a state transition to occur. + @initial_state: The state we transition FROM + @final_state: The state we are transitioning TO + @callback: A function that is run, and returns TRUE if the state transition can take place, FALSE otherwise. + """ + def __init__(self, server, initial_state, final_state, callback) -> None: + self.server: DatastreamerServer = server + self.state_a: ServerStateController.ServerState = initial_state + self.state_b: ServerStateController.ServerState = final_state + self.transition_callback = callback + + def run(self, state_a, state_b): + if(state_a == self.state_a and self.state_b == ServerStateController.ServerState.ANY): + # If transition A -> ANY + return self.transition_callback(self.server) + + if(state_b == self.state_b and self.state_a == ServerStateController.ServerState.ANY): + # If transition ANY -> B + return self.transition_callback(self.server) + + if(state_a == self.state_a and state_b == self.state_b): + # If transition A -> B + return self.transition_callback(self.server) + return True + + + transition_events: list[StateTransition] = [] + transition_always: list[Always] = [] + error_transition_events: list[StateTransition] = [] + server_state: ServerState = ServerState.INIT + transition_pipes:list[list[ServerState]] = [] + + def __init__(self) -> None: + self.server_state = ServerStateController.ServerState.INIT + + def add_transition_event(self, initial_state, final_state, callback) -> None: + self.transition_events.append(ServerStateController.StateTransition(self.server, initial_state, final_state, callback)) + + def add_always_event(self, always_target, callback) -> None: + self.transition_always.append(ServerStateController.Always(self.server, always_target, callback)) + + def try_transition(self, to_state: ServerState) -> bool: + """ + Attempt a transition, return true if successful + @to_state: State to attempt to transition to + """ + successful_transition = True + transition_checks = 0 + for transition_event in self.transition_events: + if(transition_event.state_b == to_state or transition_event.state_b == ServerStateController.ServerState.ANY): + transition_checks += 1 + if(not transition_event.run(self.server_state, to_state)): + successful_transition = False + + if successful_transition: + print("(server_state) Successfully transitioned to state", to_state, "(Passed", transition_checks, "transition checks)") + self.server_state = to_state + + return successful_transition + + def add_transition_pipe(self, from_state: ServerState, to_state: ServerState) -> None: + print("(server_state) Added pipe", from_state, " ==> ", to_state) + """ + Adds a persistent pipeline from state `from_state` to state `to_state` + (Whenever an update is called, the state machine will attempt to transfer to state `to_state` if in state `from_state`) + """ + self.transition_pipes.append([from_state, to_state]) + + def update_transitions(self) -> None: + for always_event in self.transition_always: + always_event.run(self.server_state) + for pipe in self.transition_pipes: + from_pipe = pipe[0] + to_pipe = pipe[1] + if(self.server_state == from_pipe): + if self.try_transition(to_pipe): + return + + +class DatastreamerServer: + state: ServerStateController = ServerStateController() + board_type: str = "" + server_port: serial.Serial = None + board_id = -1 + current_job: avionics.HilsimRun = None + current_job_data: dict = None + signal_abort = False + job_active = False + job_clock_reset = False + packet_buffer:packet.DataPacketBuffer = packet.DataPacketBuffer() + server_start_time = time.time() + last_server_connection_check = time.time() + next_heartbeat_time = time.time() + + +"""Singleton object for the Datastreamer server:""" +instance = DatastreamerServer() +instance.state.server = instance diff --git a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py b/Test-Rack-Software/TARS-Rack/util/handle_jobs.py new file mode 100644 index 0000000..4b83ba8 --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/util/handle_jobs.py @@ -0,0 +1,6 @@ +import util.datastreamer_server as Datastreamer +import util.packets as pkt + +def handle_job_packet(packet: pkt.DataPacket): + #TODO: implement + pass \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/handle_packets.py b/Test-Rack-Software/TARS-Rack/util/handle_packets.py new file mode 100644 index 0000000..c0f194c --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/util/handle_packets.py @@ -0,0 +1,41 @@ +import util.datastreamer_server as Datastreamer +import util.packets as pkt +import util.handle_jobs as jobs + +def add_transitions(statemachine: Datastreamer.ServerStateController): + pass + + +### Always read incoming packets regardless of state +def handle_server_packets(Server: Datastreamer.DatastreamerServer): + # Retrieve all the packets from the server input buffer + packets = Server.packet_buffer.input_buffer + for packet in packets: + # For every packet, determine what to do based on its type + match packet.packet_type: + case pkt.DataPacketType.IDENT_PROBE: + # For IDENT_PROBE, we send back the data we know + Server.packet_buffer.add(pkt.CL_ID_CONFIRM(Server.board_type, Server.board_id)) + case pkt.DataPacketType.PING: + # For PING, we send PONG. + Server.packet_buffer.add(pkt.CL_PONG()) + case pkt.DataPacketType.ACKNOWLEDGE: + # For an invalid ACK, we send back an invalid packet. + if(Server.server_port != None): + Server.packet_buffer.add(pkt.CL_INVALID(packet)) + case pkt.DataPacketType.REASSIGN: + # For REASSIGN, we check if the command is valid, reassign if yes, INVALID if no. + if(Server.board_id != -1): + Server.board_id = packet.data['board_id'] + else: + Server.packet_buffer.add(pkt.CL_INVALID(packet)) + case pkt.DataPacketType.JOB: + # For JOB, we try to trigger a job. + jobs.handle_job_packet(packet) + + + + + +def add_always_events(statemachine: Datastreamer.ServerStateController): + statemachine.add_always_event(Datastreamer.ServerStateController.ServerState.ANY, handle_server_packets) \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/packets.py b/Test-Rack-Software/TARS-Rack/util/packets.py index 9c7e427..e625178 100644 --- a/Test-Rack-Software/TARS-Rack/util/packets.py +++ b/Test-Rack-Software/TARS-Rack/util/packets.py @@ -2,83 +2,443 @@ # Contains constructor functions for each packet and also a decode function for server packets. import json +from enum import Enum +import serial + +class PacketDeserializeException(Exception): + "Raised a packet fails to deserialize" + def __init__(self, packet_string, reason="Generic deserialization error", message="Failed to deserialize packet"): + self.packet_string = packet_string + self.message = message + self.reason = reason + if len(self.packet_string) > 100: + self.packet_string = self.packet_string[0:99] + ".. (rest hidden)" + super().__init__(self.message + " " + packet_string + " ==> " + self.reason) + +class DataPacketType(int, Enum): + IDENT = 0 + ID_CONFIRM = 1 + READY = 2 + DONE = 3 + INVALID = 4 + BUSY = 5 + JOB_UPDATE = 6 + PONG = 7 + HEARTBEAT = 8 + # Server packets + IDENT_PROBE = 100 + PING = 101 + ACKNOWLEDGE = 102 + REASSIGN = 103 + TERMINATE = 104 + CYCLE = 105 + JOB = 106 + # Misc packets + RAW = 200 + +class DataPacket: + """ + A class that stores the relevant data for any type of data packet + """ + packet_type: str = "RAW" + data: dict = None + use_raw: bool = False + raw_data: str = "" + + def __init__(self, p_type:DataPacketType, p_data:dict, data_raw=None) -> None: + """ + Initializes the data packet. With raw data capabilities if raw_data is passed in. + @p_type: The type of packet + @p_data: Data stored within the packet + @data_raw: The raw string to be decoded + """ + self.packet_type = p_type + self.data = p_data + if data_raw != None: + self.use_raw = True + self.raw_data = data_raw + + def safe_deserialize(self, packet_string: str) -> None: + """ + Deserializes a packet, running through possibilities of recoverable errors if an error is encountered. + **CAREFUL!** This is an expensive operation, and should only be used if you are not sure if the stream is clear. + """ + offset = 0 + + while(offset < len(packet_string)): + try: + self.deserialize(packet_string[offset:]) + return + except: + offset += 1 + + + def deserialize(self, packet_string: str) -> None: + """ + Deserialization constructor for DataPacket + @packet_string: Raw packet string to deserialize + """ + packet_header = None + if "[[raw==>]]" in packet_string: + # If the packet uses raw: + self.use_raw = True + p_split = packet_string.split("[[raw==>]]") # Element 0 will be proper packet json, element 1 will be raw data + try: + self.data = json.loads(p_split[0])['packet_data'] + packet_header = json.loads(p_split[0]) + except: + raise PacketDeserializeException(packet_string, "Unable to deserialize packet header JSON") + self.raw_data = p_split[1] + + else: + # If the packet does not use raw: + self.use_raw = False + try: + self.data = json.loads(packet_string)['packet_data'] + packet_header = json.loads(packet_string) + except Exception as e: + raise PacketDeserializeException(packet_string, "Unable to deserialize packet header JSON") + + # Set proper enum value + if "packet_type" in packet_header: + self.packet_type = DataPacketType(packet_header['packet_type']) + else: + raise PacketDeserializeException(packet_string, "Unable to access packet_type key in packet data") + def serialize(self) -> str: + """ + Serializes one packet to a raw data string + """ + packet_dict = {'packet_type': self.packet_type, 'packet_data': self.data, 'use_raw': self.use_raw} + string_construct = json.dumps(packet_dict) + if(self.use_raw): + string_construct += "[[raw==>]]" + self.raw_data + return string_construct + + def __len__(self) -> int: + return len(self.serialize()) + + def __str__(self) -> str: + if self.use_raw: + if(len(self.raw_data) > 50): + return " " + str(self.data) + " [raw: +" + str(len(self.raw_data)) + "c]" + else: + return " " + str(self.data) + " [raw: " + self.raw_data + "]" + else: + return " " + str(self.data) + +class DataPacketBuffer: + """ + A utility class for encoding and decoding data streams with packets. + """ + packet_buffer: list[DataPacket] = [] + input_buffer: list[DataPacket] = [] + + def __init__(self, packet_list:list[DataPacket]=[]) -> None: + self.packet_buffer = packet_list + + def add(self, packet: DataPacket) -> None: + """Adds a packet to the packet buffer""" + self.packet_buffer.append(packet) + + def write_buffer_to_serial(self, serial_port: serial.Serial) -> None: + """ + Writes the entire packet buffer to serial + """ + serialized_full: str = self.to_serialized_string() + self.packet_buffer = [] + serial_port.write(serialized_full.encode()) + + + def write_packet(packet: DataPacket, serial_port: serial.Serial) -> None: + """ + Writes a single packet to serial + WARNING: using `write_packet` twice in a row will cause a deserialization error to be thrown due to a lack of a delimeter. + """ + serial_port.write(packet.serialize().encode()) + + def to_serialized_string(self) -> None: + """ + Serializes the packet buffer + """ + serialized_packets: list[str] = [] + for packet in self.packet_buffer: + serialized_packets.append(packet.serialize()) + serialized_full: str = "[[pkt_end]]".join(serialized_packets) + return serialized_full + + def stream_to_packet_list(stream: str, safe_deserialize:bool=False) -> list[DataPacket]: + """ + Converts serialized packet buffer to a list of packets + """ + serialized_packets = stream.split("[[pkt_end]]") + ds_packets: list[DataPacket] = [] + for ser_pkt in serialized_packets: + new_packet = DataPacket(DataPacketType.RAW, {}) + if(safe_deserialize): + new_packet.safe_deserialize(ser_pkt) + else: + new_packet.deserialize(ser_pkt) + + ds_packets.append(new_packet) + return ds_packets + + def read_to_input_buffer(self, port: serial.Serial) -> None: + """ + Appends all current packets in the port's buffer into the DataPacketBuffer input buffer. + """ + in_buffer = DataPacketBuffer.serial_to_packet_list(port) + for in_packet in in_buffer: + self.input_buffer.append(in_packet) + + def serial_to_packet_list(port: serial.Serial, safe_deserialize:bool=False) -> list[DataPacket]: + """ + Converts all packets in a serial port's input buffer into a list of packets. + """ + if(port.in_waiting): + instr = "" + while(port.in_waiting): + data = port.read_all() + string = data.decode("utf8") + if string: + instr += string + return DataPacketBuffer.stream_to_packet_list(instr, safe_deserialize) + return [] + + def clear_input_buffer(self): + """ + Clears this input buffer + """ + self.input_buffer = [] + + + +class JobData: + class GitPullType(int, Enum): + BRANCH = 0 # Pull a branch + COMMIT = 1 # Pull a specific commit + + class JobType(int, Enum): + DEFAULT = 0 # Pull code, flash, run hilsim, return result + DIRTY = 1 # Run hilsim with whatever is currently flashed, return result (tbd) + TEST = 2 # Run some sort of test suite (tbd) + + class JobPriority(int, Enum): + NORMAL = 0 # Normal priority, goes through queue like usual + HIGH = 1 # High priority, get priority in the queue + + + job_id:int + pull_type: GitPullType = GitPullType.BRANCH # Whether to pull from a branch or a specific commit (or other target) + pull_target: str = "master" # Which commit id / branch to pull from + job_type: JobType = JobType.DEFAULT + job_author_id: str = "" # We won't allow anonymous jobs, and we don't know what the id will look like yet, so this is a placeholder + job_priority: JobPriority = JobPriority.NORMAL # Only 2 states for now, but we can add more later if we come up with something else + job_timestep: int = 1 # How "precise" the job should be. 1 is the highest, higher numbers mean that some data points will be skipped but the job runs faster + + def __init__(self, job_id, pull_type: GitPullType=GitPullType.BRANCH, pull_target:str="master", + job_type:JobType=JobType.DEFAULT, job_author_id:str="", job_priority:JobPriority=JobPriority.NORMAL, + job_timestep:int=1) -> None: + self.job_id = job_id + self.pull_type = pull_type + self.pull_target = pull_target + self.job_type = job_type + self.job_author_id = job_author_id + self.job_priority = job_priority + self.job_timestep = job_timestep + + def to_dict(self) -> dict: + return {'job_id': self.job_id, 'pull_type': self.pull_type, 'pull_target': self.pull_target, + 'job_type': self.job_type, 'job_author_id': self.job_author_id, 'job_priority': self.job_priority, + 'job_timestep': self.job_timestep} + + + +class JobStatus: + """Static class for making job statuses""" + class JobState(int, Enum): + IDLE = 0 + ERROR = 1 + SETUP = 2 + RUNNING = 3 + + job_state: JobState = JobState.IDLE + current_action: str = "" + status_text: str = "" + def __init__(self, job_state: JobState, current_action:str, status_text:str) -> None: + self.job_state = job_state + self.current_action = current_action + self.status_text = status_text + + def to_dict(self) -> dict: + return {'job_state': self.job_state, 'current_action': self.current_action, 'status_text': self.status_text} + +class HeartbeatServerStatus: + server_state: Enum # ServerState, from main. + server_startup_time: float # (Time.time()) + is_busy: bool # Current job running, being set up, or in cleanup. + is_ready: bool # Ready for another job + + def __init__(self, server_state: Enum, server_startup_time: float, + is_busy: bool, is_ready: bool) -> None: + self.server_state = server_state + self.server_startup_time = server_startup_time + self.is_busy = is_busy + self.is_ready = is_ready + + def to_dict(self) -> dict: + return {'server_state': self.server_state, 'server_startup_time': self.server_startup_time, + 'is_busy': self.is_busy, 'is_ready': self.is_ready} + + +class HeartbeatAvionicsStatus: + connected:bool # Connected to server + avionics_type:str # May turn this into an enum + # More debug info to come + + def __init__(self, connected:bool, avionics_type:str) -> None: + self.connected = connected + self.avionics_type = avionics_type + + def to_dict(self): + return {'connected': self.connected, 'avionics_type':self.avionics_type} + #### CLIENT PACKETS #### -def construct_ident(board_type: str): - # Constructs IDENT packet - packet_dict = {'type': "IDENT", 'data': {'board_type': board_type}} - return json.dumps(packet_dict) - -def construct_id_confirm(board_type: str, board_id: int): - # Constructs ID-CONF packet - packet_dict = {'type': "ID-CONF", 'data': {'board_type': board_type, 'board_id': board_id}} - return json.dumps(packet_dict) - -def construct_ready(): - # Constructs READY packet - packet_dict = {'type': "READY", 'data': {}} - return json.dumps(packet_dict) - -def construct_done(job_data, hilsim_result: str): - # Constructs DONE packet - packet_dict = {'type': "DONE", 'data': {'job_data': job_data, 'hilsim_result': hilsim_result}} - return json.dumps(packet_dict) - -def construct_invalid(raw_packet): - # Constructs INVALID packet - packet_dict = {'type': "INVALID", 'data': {'raw_packet': raw_packet}} - return json.dumps(packet_dict) - -def construct_busy(job_data): - # Constructs BUSY packet - packet_dict = {'type': "BUSY", 'data': {'job_data': job_data}} - return json.dumps(packet_dict) - -def construct_job_update(job_status, current_log: list[str]): - # Constructs JOB-UPD packet - packet_dict = {'type': "JOB-UPD", 'data': {'job_status': job_status, 'hilsim_result': current_log}} - return json.dumps(packet_dict) - -def construct_pong(): - # Constructs PONG packet - packet_dict = {'type': "PONG", 'data': {}} - return json.dumps(packet_dict) - -#### Intermediate data #### -def construct_job_status(job_ok: bool, current_action: str, status_text: str): - return {"job_ok": job_ok, 'current_action': current_action, "status": status_text} +# Client packets are prefixed with CL +def CL_IDENT(board_type: str) -> DataPacket: + """Constructs IDENT packet + @board_type: The type of avionics stack for this packet""" + packet_data = {'board_type': board_type} + return DataPacket(DataPacketType.IDENT, packet_data) + +def CL_ID_CONFIRM(board_type: str, board_id: int) -> DataPacket: + """Constructs ID_CONFIRM packet + @board_type: The type of avionics stack connected to this server + @board_id: The ID assigned to this board before server restart.""" + packet_data = {'board_type': board_type, 'board_id': board_id} + return DataPacket(DataPacketType.ID_CONFIRM, packet_data) + +def CL_READY() -> DataPacket: + """Constructs READY packet""" + packet_data = {} + return DataPacket(DataPacketType.READY, packet_data) + +def CL_DONE(job_data: JobData, hilsim_result:str) -> DataPacket: + """Constructs DONE packet (RAW) + @job_data: The job data sent along with this packet (For ID purposes) + @hilsim_result: Raw string of the HILSIM output""" + packet_data = {'job_data': job_data.to_dict()} + return DataPacket(DataPacketType.READY, packet_data, hilsim_result) + +def CL_INVALID(invalid_packet:DataPacket) -> DataPacket: + """Constructs INVALID packet (RAW) + @raw_packet: The packet that triggered the INVALID response""" + packet_data = {} + return DataPacket(DataPacketType.INVALID, packet_data, str(invalid_packet)) + +def CL_BUSY(job_data: JobData) -> DataPacket: + """Constructs BUSY packet + @job_data: Job data for current job""" + packet_data = {'job_data': job_data.to_dict()} + return DataPacket(DataPacketType.BUSY, packet_data) + +def CL_JOB_UPDATE(job_status: JobStatus, current_log: str) -> DataPacket: + """Constructs JOB_UPDATE packet (RAW) + @job_status: State of current job + @current_log: Current outputs of HILSIM + """ + packet_data = {'job_status': job_status.to_dict()} + return DataPacket(DataPacketType.BUSY, packet_data, current_log) + +def CL_PONG() -> DataPacket: + """Constructs PONG packet""" + packet_data = {} + return DataPacket(DataPacketType.PONG, packet_data) + +def CL_HEARTBEAT(server_status: HeartbeatServerStatus, av_status: HeartbeatAvionicsStatus) -> DataPacket: + """Constructs HEARTBEAT packet + @server_status: State of the serve + @av_status: Connection and working status of avionics. + """ + packet_data = {'server_status': server_status.to_dict(), "avionics_status": av_status.to_dict()} + return DataPacket(DataPacketType.HEARTBEAT, packet_data) #### SERVER PACKETS #### -def decode_packet(packet: str): - try: - if "[raw==>]" in packet: - # This is a job packet, we treat it differently. - packet_split = packet.split("[raw==>]") - job_packet = packet_split[0] - raw_data = packet_split[1] - packet_dict = json.loads(job_packet) - packet_dict['data']['csv_data'] = raw_data - return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] - packet_dict = json.loads(packet) - except Exception as e: - return False, "RAW", packet - return validate_server_packet(packet_dict['type'], packet_dict['data']), packet_dict['type'], packet_dict['data'] - -def validate_server_packet(packet_type: str, packet_data): - match packet_type: - case "IDENT?": - return True - case "ACK": - return "board_id" in packet_data - case "REASSIGN": - return "board_id" in packet_data - case "TERMINATE": - return True - case "CYCLE": - return True - case "JOB": - return "job_data" in packet_data and "csv_data" in packet_data - case "PING": - return True - - \ No newline at end of file +# Server packets are prefixed with SV +def SV_IDENT_PROBE() -> DataPacket: + """Constructs IDENT_PROBE packet""" + packet_data = {} + return DataPacket(DataPacketType.IDENT_PROBE, packet_data) + +def SV_PING() -> DataPacket: + """Constructs PING packet""" + packet_data = {} + return DataPacket(DataPacketType.PING, packet_data) + +def SV_ACKNOWLEDGE(board_id: int) -> DataPacket: + """Constructs IDENT_PROBE packet""" + packet_data = {'board_id': board_id} + return DataPacket(DataPacketType.ACKNOWLEDGE, packet_data) + +def SV_REASSIGN(board_id: int) -> DataPacket: + """Constructs REASSIGN packet""" + packet_data = {'board_id': board_id} + return DataPacket(DataPacketType.REASSIGN, packet_data) + +def SV_TERMINATE() -> DataPacket: + """Constructs TERMINATE packet""" + packet_data = {} + return DataPacket(DataPacketType.TERMINATE, packet_data) + +def SV_CYCLE() -> DataPacket: + """Constructs CYCLE packet""" + packet_data = {} + return DataPacket(DataPacketType.CYCLE, packet_data) + +def SV_JOB(job_data: JobData, flight_csv: str) -> DataPacket: + """Constructs JOB packet""" + packet_data = {'job_data': job_data.to_dict()} + return DataPacket(DataPacketType.JOB, packet_data, flight_csv) + +class PacketValidator: + def is_server_packet(packet: DataPacket): + return packet.packet_type.value > 99 and packet.packet_type.value < 199 + + def is_client_packet(packet: DataPacket): + return packet.packet_type.value > -1 and packet.packet_type.value < 99 + + def validate_server_packet(server_packet: DataPacket): + match server_packet.packet_type: + case DataPacketType.IDENT_PROBE: + return True + case DataPacketType.ACKNOWLEDGE: + return "board_id" in server_packet.data + case DataPacketType.REASSIGN: + return "board_id" in server_packet.data + case DataPacketType.TERMINATE: + return True + case DataPacketType.CYCLE: + return True + case DataPacketType.JOB: + return "job_data" in server_packet.data and server_packet.use_raw + case DataPacketType.PING: + return True + + def validate_client_packet(client_packet: DataPacket): + match client_packet.packet_type: + case DataPacketType.IDENT: + return "board_type" in client_packet.data + case DataPacketType.ID_CONFIRM: + return "board_id" in client_packet.data and "board_type" in client_packet.data + case DataPacketType.READY: + return True + case DataPacketType.DONE: + return client_packet.use_raw and "job_data" in client_packet.data + case DataPacketType.JOB_UPDATE: + return client_packet.use_raw and "job_status" in client_packet.data + case DataPacketType.INVALID: + return client_packet.use_raw + case DataPacketType.BUSY: + return "job_data" in client_packet.data + case DataPacketType.PONG: + return True \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py index 4ffc2e8..ddca0ee 100644 --- a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py +++ b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py @@ -2,9 +2,10 @@ import serial.tools.list_ports import util.config import util.packets as packet +import json connected_comports: list[serial.Serial] = [] -packet_buffer: dict = [] +packet_buffer: dict = {} """ Function to retrieve a list of comports connected to this device @@ -28,8 +29,15 @@ def close_port(port: serial.Serial): """Clears all of the data in the port buffer""" def clear_port(port: serial.Serial): - if port.in_waiting: - data = port.read_all() + port.reset_input_buffer() + port.reset_output_buffer() + print("(clear_port) Successfully cleared port " + port.name) + +"""Clears all of the data in the port buffer""" +def hard_reset(port: serial.Serial): + port.close() + port.open() + print("(clear_port) Successfully hard reset port " + port.name) """ TODO: Delete this! @@ -65,45 +73,3 @@ def init_com_ports(): else: print(err) - -""" -Function which sends an IDENT packet to a given port. This function will be called for every single port connected to the raspi until it gets -an ACK packet from the central server. -@param port: serial.Serial -- Port to send the IDENT packet to -""" -def send_ident(port: serial.Serial): - port.write(packet.construct_ident(util.config.board_type).encode()) - -""" -The main function that handles all packets sent by the server, also returns the packet data to the caller. -""" -def handle_server_packet(port: serial.Serial, raw_packet: str): - valid_packet, packet_type, packet_data = packet.decode_packet(raw_packet) - if not valid_packet: - port.write(packet.construct_invalid(raw_packet)) - else: - # TODO: Handle server comm packets here. - pass - return valid_packet, packet_type, packet_data - - -""" -Ping all connected devices with an IDENT packet -""" -def ping_ident(): - for comport_info in get_com_ports(): - comport = serial.Serial(comport_info.device, 9600, timeout=10) - send_ident(comport) - -"""Saves a packet to be sent next write_all() call""" -def write_to_buffer(port: serial.Serial, packet: str): - global packet_buffer - if(not port.name in packet_buffer.keys()): - packet_buffer[port.name] = [] - packet_buffer[port.name].append(packet.encode()) - -"""Write all packets in a port's packet buffer""" -def write_all(port: serial.Serial): - global packet_buffer - full_data = "[packet]".join(packet_buffer[port.name]) - port.write(full_data) \ No newline at end of file From b2081d5504d6eaf89b79ff91b47aca187f4499ff Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Sat, 21 Oct 2023 16:26:56 -0500 Subject: [PATCH 10/24] job setup --- Test-Rack-Software/TARS-Rack/main.py | 13 ++-- .../TARS-Rack/util/datastreamer_server.py | 42 +++++++++++- .../TARS-Rack/util/handle_jobs.py | 65 ++++++++++++++++++- .../TARS-Rack/util/handle_packets.py | 2 +- .../TARS-Rack/util/pio_commands.py | 4 +- 5 files changed, 115 insertions(+), 11 deletions(-) diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index e097a10..ccc9ced 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -64,6 +64,13 @@ def check_server_connection(Server: Datastreamer.DatastreamerServer): if pkt.packet_type == packet.DataPacketType.ACKNOWLEDGE: Server.board_id = pkt.data['board_id'] Server.server_port = port + + #temporary, close all non-server ports: + for port in connection.connected_comports: + if(port.name != Server.server_port.name): + print("TEMP: closed " + port.name) + port.close() + return True return False @@ -112,11 +119,7 @@ def main(): while True: - if Server.server_port != None: - Server.packet_buffer.write_buffer_to_serial(Server.server_port) - Server.packet_buffer.read_to_input_buffer(Server.server_port) - - Server.state.update_transitions() # Run all transition tests + Server.tick() # Actions that always happen: if(Server.server_port != None and should_heartbeat(Server)): diff --git a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py b/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py index 10b6622..b605ff9 100644 --- a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py +++ b/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py @@ -80,6 +80,20 @@ def add_transition_event(self, initial_state, final_state, callback) -> None: def add_always_event(self, always_target, callback) -> None: self.transition_always.append(ServerStateController.Always(self.server, always_target, callback)) + def force_transition(self, to_state: ServerState) -> None: + """Same as try_transition, but doesn't check for transition_events passing or not""" + successful_transition = True + transition_checks = 0 + for transition_event in self.transition_events: + if(transition_event.state_b == to_state or transition_event.state_b == ServerStateController.ServerState.ANY): + transition_checks += 1 + transition_event.run(self.server_state, to_state) + + if successful_transition: + print("(server_state) Successfully transitioned to state", to_state, "(Executed", transition_checks, "transition functions)") + self.server_state = to_state + + def try_transition(self, to_state: ServerState) -> bool: """ Attempt a transition, return true if successful @@ -108,8 +122,6 @@ def add_transition_pipe(self, from_state: ServerState, to_state: ServerState) -> self.transition_pipes.append([from_state, to_state]) def update_transitions(self) -> None: - for always_event in self.transition_always: - always_event.run(self.server_state) for pipe in self.transition_pipes: from_pipe = pipe[0] to_pipe = pipe[1] @@ -117,22 +129,48 @@ def update_transitions(self) -> None: if self.try_transition(to_pipe): return + def update_always(self) -> None: + for always_event in self.transition_always: + always_event.run(self.server_state) class DatastreamerServer: state: ServerStateController = ServerStateController() board_type: str = "" server_port: serial.Serial = None board_id = -1 + current_job: avionics.HilsimRun = None current_job_data: dict = None + signal_abort = False job_active = False job_clock_reset = False + packet_buffer:packet.DataPacketBuffer = packet.DataPacketBuffer() server_start_time = time.time() last_server_connection_check = time.time() next_heartbeat_time = time.time() + def tick(self): + if self.server_port != None: + self.packet_buffer.write_buffer_to_serial(self.server_port) + self.packet_buffer.read_to_input_buffer(self.server_port) + + self.state.update_always() + self.state.update_transitions() # Run all transition tests + self.packet_buffer.clear_input_buffer() + + def defer(self): + """ + This function allows a single execution of the server tick from any scope that has access to the server. + This server tick does not do any transitions, since that control is held by the scope you call this function from. + """ + if self.server_port != None: + self.packet_buffer.write_buffer_to_serial(self.server_port) + self.packet_buffer.read_to_input_buffer(self.server_port) + + self.state.update_always() + self.packet_buffer.clear_input_buffer() """Singleton object for the Datastreamer server:""" instance = DatastreamerServer() diff --git a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py b/Test-Rack-Software/TARS-Rack/util/handle_jobs.py index 4b83ba8..b9cf606 100644 --- a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py +++ b/Test-Rack-Software/TARS-Rack/util/handle_jobs.py @@ -1,6 +1,69 @@ import util.datastreamer_server as Datastreamer import util.packets as pkt +import util.pio_commands as pio +import util.git_commands as git + +def run_setup_job(Server: Datastreamer.DatastreamerServer): + # This is a blocking action. Thus we will manually invoke Server.defer() between logical steps. + try: + if (Server.current_job == None): + raise Exception("Setup error: Server.current_job is not defined.") + job_data: pkt.JobData = Server.current_job + + + git.remote_reset() + git.remote_pull_branch(job_data.pull_target) + + Server.defer() # Check for abort signals + if(Server.signal_abort): + Server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False + + pio.pio_clean() + + Server.defer() # Check for abort signals + if(Server.signal_abort): + Server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False + + try: + pio.pio_upload("mcu_hilsim") + except: + Server.defer() # Check for abort signals + if(Server.signal_abort): + Server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False + pio.pio_upload("mcu_hilsim") + + return True + except: + Server.state.force_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) + return False + +def run_job(Server: Datastreamer.DatastreamerServer): + pass + + +def handle_job_transitions(statemachine: Datastreamer.ServerStateController): + SState = Datastreamer.ServerStateController.ServerState + statemachine.add_transition_event(SState.JOB_SETUP, SState.JOB_RUNNING, run_setup_job) def handle_job_packet(packet: pkt.DataPacket): - #TODO: implement + SState = Datastreamer.ServerStateController.ServerState + # If already running job or in setup: + if(Datastreamer.instance.state.server_state == SState.JOB_SETUP or Datastreamer.instance.state.server_state == SState.JOB_RUNNING or Datastreamer.instance.state.server_state == SState.JOB_ERROR): + Datastreamer.instance.packet_buffer.add(pkt.CL_INVALID(packet)) + return + + # Set up a job + job = packet.data['job_data'] + Datastreamer.instance.current_job = pkt.JobData(job['job_id'], pkt.JobData.GitPullType(job['pull_type']), + job['pull_target'], pkt.JobData.JobType(job['job_type']), + job['job_author_id'], pkt.JobData.JobPriority(job['job_priority']), + job['job_timestep']) + + Datastreamer.instance.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_SETUP) + + + pass \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/handle_packets.py b/Test-Rack-Software/TARS-Rack/util/handle_packets.py index c0f194c..65c0364 100644 --- a/Test-Rack-Software/TARS-Rack/util/handle_packets.py +++ b/Test-Rack-Software/TARS-Rack/util/handle_packets.py @@ -3,7 +3,7 @@ import util.handle_jobs as jobs def add_transitions(statemachine: Datastreamer.ServerStateController): - pass + jobs.handle_job_transitions(statemachine) ### Always read incoming packets regardless of state diff --git a/Test-Rack-Software/TARS-Rack/util/pio_commands.py b/Test-Rack-Software/TARS-Rack/util/pio_commands.py index f04b499..315333e 100644 --- a/Test-Rack-Software/TARS-Rack/util/pio_commands.py +++ b/Test-Rack-Software/TARS-Rack/util/pio_commands.py @@ -32,6 +32,6 @@ def pio_upload(build_target=None): def pio_clean(build_target=None): if(build_target == None): - run_script(['run', '--target', 'clean']) + run_script(['run', '--target', 'clean', '-s']) else: - run_script(['run', '--target', 'clean', '--environment', build_target]) + run_script(['run', '--target', 'clean', '--environment', build_target, '-s']) From dab785a936416115b0817d025c24fae64d38a006 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Mon, 23 Oct 2023 16:11:46 -0500 Subject: [PATCH 11/24] Properly goes through setup --- .gitignore | 3 +- .../API/services/datastreamer_test.py | 13 +- Central-Server/API/util/packets.py | 2 +- Test-Rack-Software/README.md | 0 .../TARS-Rack/av_platform/interface.py | 230 ++++++++++-------- Test-Rack-Software/TARS-Rack/main.py | 224 +---------------- .../TARS-Rack/util/avionics_interface.py | 90 +++++++ .../TARS-Rack/util/datastreamer_server.py | 86 ++++++- .../TARS-Rack/util/handle_jobs.py | 83 ++++--- .../TARS-Rack/util/handle_packets.py | 9 +- Test-Rack-Software/TARS-Rack/util/packets.py | 63 ++++- .../TARS-Rack/util/pio_commands.py | 4 + .../TARS-Rack/util/remote_command.py | 22 +- .../TARS-Rack/util/serial_wrapper.py | 42 ++-- 14 files changed, 443 insertions(+), 428 deletions(-) create mode 100644 Test-Rack-Software/README.md create mode 100644 Test-Rack-Software/TARS-Rack/util/avionics_interface.py diff --git a/.gitignore b/.gitignore index bfd43e2..624858b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ remote/ post_return.txt __pycache__/ -node_modules/ \ No newline at end of file +node_modules/ +.env \ No newline at end of file diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index 9033e1b..9d01c13 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -40,7 +40,6 @@ serial_tester.TEST("IDENT? packet after ACK packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.ID_CONFIRM)) cond = False - print(packet.data) res = "ID-CONF does not return correct board ID after ACK (Expected " + str(ack_test_boardid) + ", but got " + str(packet.data['board_id']) + ")" if(packet.data['board_id'] == ack_test_boardid): cond = True @@ -98,10 +97,14 @@ serial_tester.TRY_WRITE(port, pkt.SV_JOB(job_data, csv_data), "Writing JOB packet") packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.JOB_UPDATE, 30) - serial_tester.TEST("Packet after valid JOB packet complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) - job_good = data['job_status']['job_ok'] == True and data['job_status']['status'] == "Accepted" - serial_tester.TEST("Ensure job_ok is True and job_status is 'Accepted'", (job_good, f"Got job_ok {data['job_status']['job_ok']} and job_status '{data['job_status']['status']}'")) - serial_tester.TEST("Responds after building job", serial_tester.AWAIT_ANY_RESPONSE(port, 100, "(Waiting for build: This will take a while.)")) + serial_tester.TEST("Packet after valid JOB packet complies to JOB-UPD", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.JOB_UPDATE)) + + job_good = packet.data['job_status']['job_state'] == 2 and packet.data['job_status']['status_text'] == "Accepted" + serial_tester.TEST("Ensure job_state is '2' and job_status is 'Accepted'", (job_good, f"Got job_ok {packet.data['job_status']['job_state']} and job_status '{packet.data['job_status']['status_text']}'")) + + # Check for packets during setup process + + # serial_tester.TEST("Responds with any after building job", serial_tester.AWAIT_ANY_RESPONSE(port, 100, "(Waiting for build: This will take a while.)")) # Check for ok update after flash valid, type, data = serial_tester.GET_PACKET(port) diff --git a/Central-Server/API/util/packets.py b/Central-Server/API/util/packets.py index e625178..3082878 100644 --- a/Central-Server/API/util/packets.py +++ b/Central-Server/API/util/packets.py @@ -348,7 +348,7 @@ def CL_JOB_UPDATE(job_status: JobStatus, current_log: str) -> DataPacket: @current_log: Current outputs of HILSIM """ packet_data = {'job_status': job_status.to_dict()} - return DataPacket(DataPacketType.BUSY, packet_data, current_log) + return DataPacket(DataPacketType.JOB_UPDATE, packet_data, current_log) def CL_PONG() -> DataPacket: """Constructs PONG packet""" diff --git a/Test-Rack-Software/README.md b/Test-Rack-Software/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Test-Rack-Software/TARS-Rack/av_platform/interface.py b/Test-Rack-Software/TARS-Rack/av_platform/interface.py index 65d5b7c..19c2233 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/interface.py +++ b/Test-Rack-Software/TARS-Rack/av_platform/interface.py @@ -1,12 +1,10 @@ # AV Interface (TARS) # This is the specific interface.py file for the TARS avionics stack. -# @implements handle_raw(str raw_string) -- Determines what to do when a raw string is recieved from the stack. -# @implements detect_avionics(serial.Serial[] ignorePorts) -- Scans all ports for a connection, sets ready status if connected to an avionics stack. -# @implements first_setup() -- Performs the first setup (assuming fresh install). -# @implements code_reset() -- Resets the project to a "known" state (Usually master branch). -# @implements code_pull(str git_branch) -- Sets up the project to do a code flash. -# @implements code_flash() -- Flashes the code to the avionics stack -# @implements HilsimRun -- Class that stores the data for a Hilsim run +# Implements all methods in avionics_interface +# All implementations of avionics_interface must expose an `av_instance` variable which is an instance of +# a class which derives from AvionicsInterface (from avionics_interface.py). +# HilsimRun does not need to be exposed, but must derive from avionics_interface.HilsimRun +# Michael Karpov (2027) import util.git_commands as git import util.pio_commands as pio @@ -16,76 +14,55 @@ import io import time import serial -import util.packets as packet +import util.packets as pkt import traceback +import util.avionics_interface as AVInterface +import util.datastreamer_server as Datastreamer -ready = False # Is the stack ready to recieve data? -TARS_port: serial.Serial = None - -"""This function handles all raw input BEFORE all initialization is complete.""" -# Doesn't do anything for TARS, but other boards may have initialization packets. -def handle_raw(raw_string: str): - pass - -"""This function will check if there's any serial plugged into this board that is NOT the server. -TARS only has one board, so we're good if we see any other port""" -def detect_avionics(ignore_ports: list[serial.Serial], connected_to_server: bool): - global ready, TARS_port - # For TARS, we need to make sure that we're already connected to the server - if(not connected_to_server): - ready = False - return - print("(detect_avionics) Attempting to detect avionics") - for comport in server.connected_comports: - if not (comport in ignore_ports): - print("(detect_avionics) Detected viable target @ " + comport.name) - TARS_port = comport - ready = True - +class TARSAvionics(AVInterface.AvionicsInterface): + TARS_port: serial.Serial = None -""" -This function must be implemented in all run_setup.py functions for each stack -first_setup(): Installs repository and sets up all actions outside of the repository to be ready to accept inputs. -""" -def first_setup(): - git.remote_clone() - git.remote_reset() - -""" -This function must be implemented in all run_setup.py functions for each stack -code_reset(): Resets the repository to a default state -TARS: Resets the TARS-Software repository to master branchd -""" -def code_reset(): - git.remote_reset() - # Clean build dir - pio.pio_clean() - -""" -This function must be implemented in all run_setup.py functions for each stack -code_pull(str git_branch): Stashes changes and pulls a specific branch. -""" -def code_pull(git_branch: str): - git.remote_pull_branch(git_branch) - -""" -This function must be implemented in all run_setup.py functions for each stack -code_flash(): Flashes currently staged code to the avionics stack. -TARS: Uses environment mcu_hilsim -""" -def code_flash(): - # For TARS, we need to attempt the code flash twice, since it always fails the first time. - try: - pio.pio_upload("mcu_hilsim") - except: - pio.pio_upload("mcu_hilsim") - - -""" -This object stores all of the required information and functionality of a HILSIM run. This class -must be implemented in all run_setup.py scripts. -""" -class HilsimRun: + # Doesn't do anything for TARS, but other boards may have initialization packets + def handle_init(self) -> None: + return super().handle_init() + + def detect(self) -> bool: + # For TARS, we need to make sure that we're already connected to the server + if(not self.server): + self.ready = False + return + print("(detect_avionics) Attempting to detect avionics") + + ignore_ports = [self.server.server_port] + + for comport in server.connected_comports: + if not (comport in ignore_ports): + print("(detect_avionics) Detected viable target @ " + comport.name) + self.TARS_port = comport + self.ready = True + + def first_setup(self) -> None: + git.remote_clone() + git.remote_reset() + + def code_reset(self) -> None: + git.remote_reset() + # Clean build dir + pio.pio_clean() + + def code_pull(self, git_target) -> None: + git.remote_pull_branch(git_target) + + def code_flash(self) -> None: + """Flashes code to the stack. For TARS, uses environment `mcu_hilsim`""" + # For TARS, we need to attempt the code flash twice, since it always fails the first time. + try: + pio.pio_upload("mcu_hilsim") + except: + pio.pio_upload("mcu_hilsim") + +class HilsimRun(AVInterface.HilsimRunInterface): + av_interface: TARSAvionics # Specify av_interface is TARS-specific! return_log = [] flight_data_raw = "" flight_data_dataframe = None @@ -97,34 +74,62 @@ class HilsimRun: port = None job_data = None - # Getter for current log - def get_current_log(self): + def get_current_log(self) -> str: return self.return_log - # Sets up job to run (Cannot be canceled) - # @param cancel_callback: Function that returns whether the job should be terminated. - def job_setup(self, cancel_callback): - job = self.job_data + def job_setup(self): + if (self.job == None): + raise Exception("Setup error: Server.current_job is not defined.") + # Temporarily close port so code can flash - TARS_port.close() - if(job['pull_type'] == "branch"): + # self.av_interface.TARS_port.close() + + if(self.job.pull_type == pkt.JobData.GitPullType.BRANCH): try: - code_reset() - if(cancel_callback()): - return False, "Terminate signal sent during setup" - code_pull(job['pull_target']) - if(cancel_callback()): - return False, "Terminate signal sent during setup" - code_flash() + + self.av_interface.code_reset() + + # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) + self.server.defer() + if(self.server.signal_abort): + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False, "Abort signal recieved" + + self.av_interface.code_pull(self.job.pull_target) + job = self.server.current_job_data + accepted_status = pkt.JobStatus(pkt.JobStatus.JobState.SETUP, "COMPILE_READY", f"Finished pre-compile setup on job {str(job.job_id)}") + self.server.packet_buffer.add(pkt.CL_JOB_UPDATE(accepted_status, "")) + + # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) + self.server.defer() + if(self.server.signal_abort): + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False, "Abort signal recieved" + + self.av_interface.code_flash() + + job = self.server.current_job_data + accepted_status = pkt.JobStatus(pkt.JobStatus.JobState.SETUP, "COMPILED", f"Finished code flash on job {str(job.job_id)}") + self.server.packet_buffer.add(pkt.CL_JOB_UPDATE(accepted_status, "")) + + # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) + self.server.defer() + if(self.server.signal_abort): + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False, "Abort signal recieved" # Wait for the port to open back up (Max wait 10s) start = time.time() while(time.time() < start + 10): - if(cancel_callback()): - return False, "Terminate signal sent during COMPort setup" + # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) + self.server.defer() + if(self.server.signal_abort): + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + return False, "Abort signal recieved" + try: - TARS_port.open() - print("\n(job_setup) Successfully re-opened TARS port (" + TARS_port.name + ")") + self.av_interface.TARS_port.open() + print("\n(job_setup) Successfully re-opened TARS port (" + self.av_interface.TARS_port.name + ")") return True, "Setup Complete" except: time_left = abs((start + 10) - time.time()) @@ -133,9 +138,8 @@ def job_setup(self, cancel_callback): except Exception as e: return False, "Setup failed: " + str(e) - elif (job['pull_type'] == "commit"): - # Not implemented yet - pass + elif (self.job.pull_type == pkt.JobData.GitPullType.COMMIT): + raise NotImplementedError("Commit-based pulls are not implemented yet.") # Turns a raw CSV string to a Pandas dataframe @@ -145,14 +149,15 @@ def raw_csv_to_dataframe(self, raw_csv) -> pandas.DataFrame: csv = "\n".join(raw_csv.split('\n')[1:]) csvStringIO = io.StringIO(csv) return pandas.read_csv(csvStringIO, sep=",", header=None, names=header) + # Initializes the HILSIM run object - def __init__(self, raw_csv: str, job: dict) -> None: - global TARS_port + def __init__(self, datastreamer: Datastreamer.DatastreamerServer, av_interface: TARSAvionics, raw_csv: str, job: pkt.JobData) -> None: + super().__init__(datastreamer, av_interface, raw_csv, job) self.flight_data_raw = raw_csv self.flight_data_dataframe = self.raw_csv_to_dataframe(self.flight_data_raw) self.flight_data_rows = self.flight_data_dataframe.iterrows() - self.port = TARS_port + self.port = av_interface.TARS_port self.start_time = time.time() self.current_time = self.start_time self.last_packet_time = self.start_time @@ -165,13 +170,14 @@ def reset_clock(self): """ Runs one iteration of the HILSIM loop, with a change in time of dt. - callback_func is a function to communicate back to the main process mid-step. @Returns a tuple: (run_finished, run_errored, return_log) """ - def step(self, dt: float, callback_func): + def step(self, dt: float): self.current_time += dt simulation_dt = 0.01 + # The av stack can only take a certain amount of data at a time, so we need to yield until we + # can safely send data. if self.current_time > self.last_packet_time + simulation_dt: self.last_packet_time += simulation_dt @@ -180,25 +186,35 @@ def step(self, dt: float, callback_func): pass else: if(self.current_line == 0): - callback_func(packet.construct_job_status(True, "running", f"Running (Data streaming started)")) + job_status = pkt.JobStatus(pkt.JobStatus.JobState.RUNNING, "BEGIN", f"Running (Data streaming started)") + status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) + self.av_interface.server.packet_buffer.add(status_packet) self.current_line += 1 if(self.current_line % 300 == 0): # Only send a job update every 3-ish seconds - callback_func(packet.construct_job_status(True, "running", f"Running ({self.current_line/len(self.flight_data_dataframe)*100:.2f}%) [{self.current_line} processed out of {len(self.flight_data_dataframe)} total]")) + job_status = pkt.JobStatus(pkt.JobStatus.JobState.RUNNING, "RUNNING", f"Running ({self.current_line/len(self.flight_data_dataframe)*100:.2f}%) [{self.current_line} processed out of {len(self.flight_data_dataframe)} total]") + status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) + self.av_interface.server.packet_buffer.add(status_packet) line_num, row = next(self.flight_data_rows, (None, None)) if line_num == None: - callback_func(packet.construct_job_status(True, "done", f"Finished data streaming")) + job_status = pkt.JobStatus(pkt.JobStatus.JobState.RUNNING, "RUNNING", f"Finished data streaming") + status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) + self.av_interface.server.packet_buffer.add(status_packet) return True, False, self.return_log # Finished, No Error, Log data = csv_datastream.csv_line_to_protobuf(row) if not data: - callback_func(packet.construct_job_status(True, "error", f"Expected data to insert, but found none.")) + job_status = pkt.JobStatus(pkt.JobStatus.JobState.ERROR, "ABORTED_ERROR", f"Expected data to insert, but found none") + status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) + self.av_interface.server.packet_buffer.add(status_packet) return True, False, self.return_log # Finished, Error, Log try: self.port.write(data) except: - callback_func(packet.construct_job_status(True, "error", f"Exception during serial write: " + traceback.format_exc())) + job_status = pkt.JobStatus(pkt.JobStatus.JobState.ERROR, "ABORTED_ERROR", f"Exception during serial write: " + traceback.format_exc()) + status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) + self.av_interface.server.packet_buffer.add(status_packet) return True, False, self.return_log # Finished, Error, Log if self.port.in_waiting: @@ -211,4 +227,4 @@ def step(self, dt: float, callback_func): return False, False, self.return_log - +av_instance = TARSAvionics(Datastreamer.instance) \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/TARS-Rack/main.py index ccc9ced..42c90db 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/TARS-Rack/main.py @@ -35,9 +35,9 @@ def handle_job_runtime_error(Server: Datastreamer.DatastreamerServer): def handle_first_setup(Server: Datastreamer.DatastreamerServer): try: Server.board_type = config.board_type - avionics.first_setup() + avionics.av_instance.first_setup() return True - except: + except Exception as e: return False def should_heartbeat(Server: Datastreamer.DatastreamerServer): @@ -127,229 +127,9 @@ def main(): Server.packet_buffer.add(packet.CL_HEARTBEAT(packet.HeartbeatServerStatus(Server.state.server_state, Server.server_start_time, False, False), packet.HeartbeatAvionicsStatus(False, ""))) # End "Always" actions. - Server.packet_buffer.clear_input_buffer() -def send_ready(): - global server_port - connection.write_to_buffer(server_port, packet.construct_ready()) - connection.write_all(server_port) - -def setup_job(job_packet_data): - global current_job - while(server_port.in_waiting): - data = server_port.read_all() - string = data.decode("utf8") - if string: - job_packet_data['csv_data'] += string - - current_job = avionics.HilsimRun(job_packet_data['csv_data'], job_packet_data['job_data']) - current_job_data = job_packet_data['job_data'] - -def handle_server_packet(packet_type, packet_data, packet_string, comport: serial.Serial): - global server_port, job_active, signal_abort - if(packet_type != "JOB"): - print("Handling packet << " + packet_string) - else: - print("Handling packet << " + packet_type + f" ({packet_data['job_data']}) + [hidden csv data]") - global server_port, board_id - if packet_type == "ACK": - if server_port == None: - server_port = comport - board_id = packet_data['board_id'] - print("(datastreamer) Connected to server as board " + str(board_id) + "!") - return - else: - print("Recieved ACK packets from more than one source! Discarding second ACK.") - comport.write(packet.construct_invalid(packet_string).encode()) - return - - log_string = "" - match packet_type: - case "IDENT?": - comport.write(packet.construct_id_confirm(config.board_type, board_id).encode()) - log_string += "Sending packet >> " + packet.construct_id_confirm(config.board_type, board_id) - case "PING": - comport.write(packet.construct_pong().encode()) - log_string += "Sending packet >> " + packet.construct_pong() - case "REASSIGN": - if(board_id != -1): - new_id = packet_data['board_id'] - log_string += "Reassigned board ID (" + str(board_id) + " ==> " + str(new_id) + ")" - board_id = new_id - else: - comport.write(packet.construct_invalid(packet_string).encode()) - log_string += "Recieved REASSIGN packet but this board has not been initialized" - case "JOB": - try: - setup_job(packet_data) - job_status = packet.construct_job_status(True, "Setup", "Accepted") - log_string += "Job set up" - print("(handle_packet) Job accepted") - comport.write(packet.construct_job_update(job_status, []).encode()) - - # Returns true if the job should be canceled. - def cancel_job(): - read_data([server_port]) - return signal_abort - - completed, status = current_job.job_setup(cancel_job) - job_status = packet.construct_job_status(completed, "Setup", status) - comport.write(packet.construct_job_update(job_status, []).encode()) - - if(completed): - job_active = True - job_clock_reset = False - elif (signal_abort): - send_ready() - except Exception as e: - log_string += "Job rejected: " + str(e) - comport.write(packet.construct_invalid(packet_string).encode()) - case "TERMINATE": - # Terminate currently running job - signal_abort = True - - if(log_string == ""): - log_string = "(handle_packet) No special log string for pkt\n" + packet_string - else: - log_string = "(handle_packet) " + log_string - print(log_string) - -def read_data(listener_list: list[serial.Serial]): - # Process all incoming packets (Caution! Will process EVERYTHING before server is connected!) - for comport in listener_list: - if comport.in_waiting: - data = comport.read_all() - packet_string = data.decode("utf8") - valid, type, data = packet.decode_packet(packet_string) - if not valid: - # If it's not valid, it's either not a valid packet, or type="RAW" (just a string) - if type == "RAW": - avionics.handle_raw(packet_string) - else: - # invalid packet - comport.write(packet.construct_invalid(packet_string).encode()) - else: - # Is server packet - handle_server_packet(type, data, packet_string, comport) - -def main_old(): - global server_port, board_id, job_active, job_clock_reset - # TODO: Implement application-specific setup - # TODO: Implement stack-specific setup - # TODO: Implement Server-application communication (Serial communication) - # TODO: Testing? - - # Initialize repository - # avionics.first_setup() - - next_conn_debounce = time.time() + 0.5 - next_av_debounce = time.time() + 0.1 - last_job_loop_time = time.time() - - print("(datastreamer) first setup done") - while True: - # Are we in init state? (Don't have a server connected) - if server_port == None and time.time() > next_conn_debounce: - print("(datastreamer) Missing server connection! Running server probe") - connection.t_init_com_ports() - for port in connection.connected_comports: - connection.send_ident(port) - next_conn_debounce = time.time() + 0.5 - - - if not avionics.ready and time.time() > next_av_debounce: - avionics.detect_avionics([server_port], server_port != None) - - # Uncomment line below if testing with actual avionics - # avionics.ready = True - - if(avionics.ready): - print("(datastreamer) Avionics ready!") - next_av_debounce = time.time() + 0.1 - - listener_list = connection.connected_comports - if(server_port != None): - listener_list = [server_port] - - # Reads all comport data and acts on it - read_data(listener_list) - - def send_running_job_status(job_status_packet): - server_port.write(packet.construct_job_update(job_status_packet, current_job.get_current_log()).encode()) - print(f"(current_job_status) Is_OK: {str(job_status_packet['job_ok'])}, Current action: {job_status_packet['current_action']}, Status: {job_status_packet['status']}") - - # Runs currently active job (If any) - if(job_active): - if(signal_abort): - # Stop job immediately and notify server of status - job_active = False - job_status = packet.construct_job_status(False, "Stopped", "The job has been forcefully stopped") - print("(run_job) Job terminated") - - connection.write_to_buffer(server_port, packet.construct_job_update(job_status, current_job.get_current_log())) - send_ready() - continue - - if(job_clock_reset == False): - current_job.reset_clock() - job_clock_reset = True - last_job_loop_time = time.time() - - - if(last_job_loop_time != time.time()): - dt = time.time() - last_job_loop_time - run_finished, run_errored, cur_log = current_job.step(dt, send_running_job_status) - - if(run_finished): - job_active = False - pass - - if(run_errored): - job_active = False - pass - - last_job_loop_time = time.time() - - - - - - - - - - - - print("(main) Waiting 5s for port to open back up") - time.sleep(5) # Wait for port to start back up - - s.init_com_ports() - port = getfirstport() - if(not port): - return - - # Run hilsim (jank for now) - file = open(os.path.join(os.path.dirname(__file__), "./av_platform/flight_computer.csv"), 'r') - text = file.read() - - print() - def noout(t): - print(t['status'], end='\r') - hilsim_result = platform.run_hilsim(text, port, noout) - print() - - logfile = os.path.join(os.path.dirname(__file__), "./post_return.txt") - - f = open(logfile, "w", encoding="utf-8") - f.write(hilsim_result.replace("\r\n", "\n")) - print("(main) Finished remote hilsim run!") - git.remote_reset() - - pass - - ########################################################## if __name__ == "__main__": diff --git a/Test-Rack-Software/TARS-Rack/util/avionics_interface.py b/Test-Rack-Software/TARS-Rack/util/avionics_interface.py new file mode 100644 index 0000000..2ad51af --- /dev/null +++ b/Test-Rack-Software/TARS-Rack/util/avionics_interface.py @@ -0,0 +1,90 @@ +import util.datastreamer_server as Datastreamer +import util.packets as pkt +from abc import ABC, abstractmethod # ABC = Abstract Base Classes + + +class AvionicsInterface(ABC): + """An interface class for all av_interface stacks. Allows for standardization of HILSIM runs. + Provides basic functionality, but stacks will have to implement their own methods.""" + ready: bool = False # Is this avionics stack ready? + server: Datastreamer.DatastreamerServer = None + + def __init__(self, datastreamer: Datastreamer.DatastreamerServer) -> None: + server = datastreamer + + @abstractmethod + def handle_init(self) -> None: + """Some boards may have initialization packets, this function will handle + those packets and perform any av-stack specific setup.""" + raise NotImplementedError("AvionicsInterface.handle_init method not implemented") + + @abstractmethod + def detect(self) -> bool: + """Detects any specific avionics stack. Updates `ready` depending on the result. + Additionally, returns the result.""" + raise NotImplementedError("AvionicsInterface.detect method not implemented") + + @abstractmethod + def first_setup(self) -> None: + """Performs any specific setup for this stack.""" + raise NotImplementedError("AvionicsInterface.first_setup method not implemented") + + @abstractmethod + def code_reset(self) -> None: + """Resets the repo and stack to its default state""" + raise NotImplementedError("AvionicsInterface.code_reset method not implemented") + + @abstractmethod + def code_pull(self, git_target: str) -> None: + """Stashes current changes and pulls a specific branch or commit""" + raise NotImplementedError("AvionicsInterface.code_pull method not implemented") + + @abstractmethod + def code_flash(self) -> None: + """Flashes currently staged code to the AV stack""" + raise NotImplementedError("AvionicsInterface.code_flash method not implemented") + +class HilsimRunInterface(ABC): + """The server to defer to""" + server: Datastreamer.DatastreamerServer = None + + """The avionics stack to refer to when performing runs and setup""" + av_interface: AvionicsInterface = None + + """The csv data to stream to the stack, as a string""" + raw_data: str + + """JobData packet data""" + job: pkt.JobData = None + + @abstractmethod + def get_current_log(self) -> str: + """Returns the current log output from the AV stack""" + raise NotImplementedError("AvionicsInterface.HilsimRun.get_current_log method not implemented") + + @abstractmethod + def job_setup(self) -> tuple[bool, str]: + """Sets up a job to run (This code will usually be blocking, so it will have to manually call server.defer()!) + + + @returns A tuple: [job_setup_successful, job_setup_fail_reason]""" + raise NotImplementedError("AvionicsInterface.HilsimRun.job_setup method not implemented") + + # Initializes the HILSIM run object + def __init__(self, datastreamer: Datastreamer.DatastreamerServer, av_interface: AvionicsInterface, raw_csv: str, job: pkt.JobData) -> None: + """Base constructor for the HILSIM run object. Children may have additional setup.""" + self.raw_data = raw_csv + self.av_interface = av_interface + self.job = job + self.server = datastreamer + + @abstractmethod + def step(self, dt: float, send_status) -> tuple[bool, bool, str]: + """ + Runs one iteration of the HILSIM loop, with a change in time of dt. + + @Returns a tuple: (run_finished, run_errored, return_log) + """ + raise NotImplementedError("AvionicsInterface.HilsimRun.step method not implemented") + + diff --git a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py b/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py index b605ff9..dcef812 100644 --- a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py +++ b/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py @@ -1,6 +1,7 @@ import sys import os +# Make sure ../util and ../av_platform paths can be accessed sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) @@ -11,29 +12,52 @@ from enum import Enum class ServerStateController(): - # A state machine for the server + """A class representing the server's state as a state machine. + + Consists of a few main components: + + @`Pipes (A=>B)` are transitions (to state B) that the server is constantly attempting to perform if it's in state A + @`Transition events` are blocks of code that are run when specific transitions are performed. If using `try_transition`, they can also dictate whether the transition succeeds. + @`Always events` are blocks of code that are executed each tick when on a specific state""" server = None class ServerState(int, Enum): + "Enum describing possible server states as an integer." ANY = -1 + """Any server state""" INIT = 0 + """State when datastreamer is initializing systems""" CONNECTING = 1 # Before server connection + """State when datastreamer is detecting a Kamaji server""" AV_DETECT = 2 # Before detecting avionics + """State when datastreamer is detecting an avionics stack""" READY = 3 # Ready to recieve jobs + """State when datastreamer is ready for jobs/idle""" JOB_SETUP = 4 # Recieved a job, in setup + """State when datastreamer is performing setup to run a job""" JOB_RUNNING = 5 # Running a job + """State when datastreamer is actively running a job""" CLEANUP = 6 # Finished job, cleaning up environment + """State after a job is done/after a job fails, to clean up the environment""" ERROR = 100 # Error state (recoverable) + """Fail state (recoverable)""" FERROR = 101 # Error state (unrecoverable) + """Fatal fail state (non-recoverable)""" JOB_ERROR = 102 # Error during a job + """Job fail state (recoverable), used for logging errors when a job fails during setup or run""" class Always: """A class representing an action that should always be run in a certain state""" def __init__(self, server, target_state, callback) -> None: + """Initialize an `Always event` + @server {DatastreamerServer} -- The server to use + @target_state {ServerStateController.ServerState} -- The state during which to run the callback + @callback {lambda} -- The callback to call when the Always event is run.""" self.server: DatastreamerServer = server self.target_state: ServerStateController.ServerState = target_state self.callback = callback def run(self, current_state): + """Determine if this callback should be run or not""" if(self.target_state == current_state or self.target_state == ServerStateController.ServerState.ANY): self.callback(self.server) @@ -72,26 +96,31 @@ def run(self, state_a, state_b): transition_pipes:list[list[ServerState]] = [] def __init__(self) -> None: + """Initialize the state machine with default state ServerState.INIT""" self.server_state = ServerStateController.ServerState.INIT def add_transition_event(self, initial_state, final_state, callback) -> None: + """Add a `transition_event` to the state machine""" self.transition_events.append(ServerStateController.StateTransition(self.server, initial_state, final_state, callback)) def add_always_event(self, always_target, callback) -> None: + """Add an `always_event` to the state machine""" self.transition_always.append(ServerStateController.Always(self.server, always_target, callback)) def force_transition(self, to_state: ServerState) -> None: """Same as try_transition, but doesn't check for transition_events passing or not""" - successful_transition = True transition_checks = 0 for transition_event in self.transition_events: + # For each transition event, we check if its to_state matches the state we're forcing the transition to. + # If yes, we execute the code but discard the result. if(transition_event.state_b == to_state or transition_event.state_b == ServerStateController.ServerState.ANY): transition_checks += 1 + transition_event.run(self.server_state, to_state) - if successful_transition: - print("(server_state) Successfully transitioned to state", to_state, "(Executed", transition_checks, "transition functions)") - self.server_state = to_state + # Report transition + print("(server_state) Successfully transitioned to state", to_state, "(Executed", transition_checks, "transition functions)") + self.server_state = to_state def try_transition(self, to_state: ServerState) -> bool: @@ -102,11 +131,16 @@ def try_transition(self, to_state: ServerState) -> bool: successful_transition = True transition_checks = 0 for transition_event in self.transition_events: + # For each transition event, we check if its to_state matches what state we're attempting to transition to. if(transition_event.state_b == to_state or transition_event.state_b == ServerStateController.ServerState.ANY): transition_checks += 1 + # If yes, we check if the transition event allows us to transition. + + #transition_event.run performs its own run checks. if(not transition_event.run(self.server_state, to_state)): successful_transition = False + # If all transition checks succeed, we transition to the new state. if successful_transition: print("(server_state) Successfully transitioned to state", to_state, "(Passed", transition_checks, "transition checks)") self.server_state = to_state @@ -122,43 +156,66 @@ def add_transition_pipe(self, from_state: ServerState, to_state: ServerState) -> self.transition_pipes.append([from_state, to_state]) def update_transitions(self) -> None: + """Attempt to perform all pipe transitions defined in self.transition_pipes. Returns early if one is successful""" for pipe in self.transition_pipes: from_pipe = pipe[0] to_pipe = pipe[1] + # If a pipe exists from the current state, we try to transition to its to_state if(self.server_state == from_pipe): if self.try_transition(to_pipe): return def update_always(self) -> None: + """Runs all `Always events` defined in self.transition_always""" for always_event in self.transition_always: always_event.run(self.server_state) class DatastreamerServer: + """A singleton-designed class that holds all relevant information about the Datastreamer.""" state: ServerStateController = ServerStateController() + """Datastreamer server's state machine reference""" board_type: str = "" server_port: serial.Serial = None + """Serial port reference to the serial port that connects to the Kamaji server.""" board_id = -1 + """Board ID assigned by the Kamaji server""" - current_job: avionics.HilsimRun = None - current_job_data: dict = None + current_job = None # HilsimRun + """The job that is currently being setup/run""" + current_job_data: packet.JobData = None + """The data for the current job""" signal_abort = False + """Standin for a process SIGABRT. If set to true, jobs will attempt to stop as soon as it's gracefully possible to do so""" job_active = False + """Boolean to check if a job is currently being run""" job_clock_reset = False + """Boolean that forces a job's internal clock to reset. WARNING: Doing so in the middle of a job will cause overwrites""" packet_buffer:packet.DataPacketBuffer = packet.DataPacketBuffer() + """Reference to the server's packet buffer, a utility object that helps handle packets""" server_start_time = time.time() + """Time that this server started""" last_server_connection_check = time.time() + """Time of last heartbeat packet""" next_heartbeat_time = time.time() + """Time when next heartbeat packet will be sent.""" + + last_job_step_time = time.time() + """The last time that a job step() was completed""" def tick(self): + """Generic single action on the server. Will perform all server actions by calling transition events and always events. + + This function will generally be run within an infinite while loop.""" if self.server_port != None: + # We clear out output buffer and also populate our input buffer from the server self.packet_buffer.write_buffer_to_serial(self.server_port) self.packet_buffer.read_to_input_buffer(self.server_port) - self.state.update_always() - self.state.update_transitions() # Run all transition tests - self.packet_buffer.clear_input_buffer() + self.state.update_always() # Run all `always` events + self.state.update_transitions() # Run all transition tests + self.packet_buffer.clear_input_buffer() # Discard our input buffer so we can get a new one next loop def defer(self): """ @@ -166,12 +223,15 @@ def defer(self): This server tick does not do any transitions, since that control is held by the scope you call this function from. """ if self.server_port != None: + # We clear out output buffer and also populate our input buffer from the server self.packet_buffer.write_buffer_to_serial(self.server_port) self.packet_buffer.read_to_input_buffer(self.server_port) - self.state.update_always() + self.state.update_always() # Run all `always` events self.packet_buffer.clear_input_buffer() -"""Singleton object for the Datastreamer server:""" + instance = DatastreamerServer() -instance.state.server = instance +"""Singleton object for the Datastreamer server""" + +instance.state.server = instance # Set the serverstate's internal server object to a cyclical reference. diff --git a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py b/Test-Rack-Software/TARS-Rack/util/handle_jobs.py index b9cf606..d98361f 100644 --- a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py +++ b/Test-Rack-Software/TARS-Rack/util/handle_jobs.py @@ -1,54 +1,61 @@ import util.datastreamer_server as Datastreamer import util.packets as pkt -import util.pio_commands as pio -import util.git_commands as git +import av_platform.interface as avionics +import util.avionics_interface as AVInterface +import time def run_setup_job(Server: Datastreamer.DatastreamerServer): - # This is a blocking action. Thus we will manually invoke Server.defer() between logical steps. + """Invokes the avionics system's job setup method. + + Avionics setup methods are generally blocking. Make sure that they properly call Server.defer() when possible.""" try: - if (Server.current_job == None): - raise Exception("Setup error: Server.current_job is not defined.") - job_data: pkt.JobData = Server.current_job - - - git.remote_reset() - git.remote_pull_branch(job_data.pull_target) - - Server.defer() # Check for abort signals - if(Server.signal_abort): - Server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) - return False - - pio.pio_clean() - - Server.defer() # Check for abort signals - if(Server.signal_abort): - Server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) - return False + job = Server.current_job_data + accepted_status = pkt.JobStatus(pkt.JobStatus.JobState.SETUP, "Setting up job " + str(job.job_id), "Accepted") + Server.packet_buffer.add(pkt.CL_JOB_UPDATE(accepted_status, "")) + Server.defer() - try: - pio.pio_upload("mcu_hilsim") - except: - Server.defer() # Check for abort signals - if(Server.signal_abort): - Server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) - return False - pio.pio_upload("mcu_hilsim") - - return True - except: + current_job: AVInterface.HilsimRunInterface = Server.current_job # For type hints + setup_successful, setup_fail_reason = current_job.job_setup() + if(setup_successful): + return True + else: + raise Exception("Setup failed: " + setup_fail_reason) + except Exception as e: + print(e) Server.state.force_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) return False def run_job(Server: Datastreamer.DatastreamerServer): + """Invokes the step() method in the current HilsimRun (plaform-blind)""" + dt = time.time() - Server.last_job_step_time + run_finished, run_errored, return_log = Server.current_job.step(dt) + if(run_finished): + if(run_errored): + Server.state.force_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) + return False + else: + # The job has successfully completed! Inform the server of this fact + # TODO: report job finish status to server + return True + + Server.last_job_step_time = time.time() + +def handle_job_setup_error(Server: Datastreamer.DatastreamerServer): pass +def handle_job_runtime_error(Server: Datastreamer.DatastreamerServer): + pass def handle_job_transitions(statemachine: Datastreamer.ServerStateController): + """Add transition events for jobs""" SState = Datastreamer.ServerStateController.ServerState statemachine.add_transition_event(SState.JOB_SETUP, SState.JOB_RUNNING, run_setup_job) + statemachine.add_transition_event(SState.JOB_RUNNING, SState.CLEANUP, run_job) + statemachine.add_transition_event(SState.JOB_SETUP, SState.JOB_ERROR, handle_job_setup_error) + statemachine.add_transition_event(SState.JOB_RUNNING, SState.JOB_ERROR, handle_job_runtime_error) def handle_job_packet(packet: pkt.DataPacket): + """Handles all job packets sent by the Kamaji server""" SState = Datastreamer.ServerStateController.ServerState # If already running job or in setup: if(Datastreamer.instance.state.server_state == SState.JOB_SETUP or Datastreamer.instance.state.server_state == SState.JOB_RUNNING or Datastreamer.instance.state.server_state == SState.JOB_ERROR): @@ -57,13 +64,11 @@ def handle_job_packet(packet: pkt.DataPacket): # Set up a job job = packet.data['job_data'] - Datastreamer.instance.current_job = pkt.JobData(job['job_id'], pkt.JobData.GitPullType(job['pull_type']), + raw_csv = packet.raw_data + Datastreamer.instance.current_job_data = pkt.JobData(job['job_id'], pkt.JobData.GitPullType(job['pull_type']), job['pull_target'], pkt.JobData.JobType(job['job_type']), job['job_author_id'], pkt.JobData.JobPriority(job['job_priority']), job['job_timestep']) + Datastreamer.instance.current_job = avionics.HilsimRun(Datastreamer.instance, avionics.av_instance, raw_csv, Datastreamer.instance.current_job_data) - Datastreamer.instance.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_SETUP) - - - - pass \ No newline at end of file + Datastreamer.instance.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_SETUP) \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/handle_packets.py b/Test-Rack-Software/TARS-Rack/util/handle_packets.py index 65c0364..3af69ac 100644 --- a/Test-Rack-Software/TARS-Rack/util/handle_packets.py +++ b/Test-Rack-Software/TARS-Rack/util/handle_packets.py @@ -1,13 +1,17 @@ +# This script handles all incoming packets as an ALWAYS action on the ANY state. + import util.datastreamer_server as Datastreamer import util.packets as pkt import util.handle_jobs as jobs def add_transitions(statemachine: Datastreamer.ServerStateController): + """Add transition events for all packets""" jobs.handle_job_transitions(statemachine) ### Always read incoming packets regardless of state def handle_server_packets(Server: Datastreamer.DatastreamerServer): + """Callback to handle all packets""" # Retrieve all the packets from the server input buffer packets = Server.packet_buffer.input_buffer for packet in packets: @@ -34,8 +38,9 @@ def handle_server_packets(Server: Datastreamer.DatastreamerServer): jobs.handle_job_packet(packet) - - def add_always_events(statemachine: Datastreamer.ServerStateController): + """Add the always events for packets. + + Because of the nature of how we want to handle packets, we ALWAYS listen to packets regardless of server state.""" statemachine.add_always_event(Datastreamer.ServerStateController.ServerState.ANY, handle_server_packets) \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/packets.py b/Test-Rack-Software/TARS-Rack/util/packets.py index e625178..08f08b2 100644 --- a/Test-Rack-Software/TARS-Rack/util/packets.py +++ b/Test-Rack-Software/TARS-Rack/util/packets.py @@ -12,29 +12,48 @@ def __init__(self, packet_string, reason="Generic deserialization error", messag self.message = message self.reason = reason if len(self.packet_string) > 100: + # If a packet string is over 100 characters, we truncate it self.packet_string = self.packet_string[0:99] + ".. (rest hidden)" super().__init__(self.message + " " + packet_string + " ==> " + self.reason) class DataPacketType(int, Enum): + """Enum describing the different types of packets we can send/recieve""" IDENT = 0 + """[CLIENT] Identification packet. Sent to servers to identify this as a testing rack.""" ID_CONFIRM = 1 + """[CLIENT] Identification confirmation. Sent to a server after failure to confirm this rack's identity""" READY = 2 + """[CLIENT] Sent when this board is ready for a new job""" DONE = 3 + """[CLIENT] Sent when a board is done with a job""" INVALID = 4 + """[CLIENT] Sent in response to a [SERVER] packet that's invalid""" BUSY = 5 + """[CLIENT] Sent when the client is currently busy with setup or running, and cannot complete the request""" JOB_UPDATE = 6 + """[CLIENT] Sent intermittently while a job is running""" PONG = 7 + """[CLIENT] Sent in response to a PING packet""" HEARTBEAT = 8 + """[CLIENT] Sent to update the Kamaji server of this board's status""" # Server packets IDENT_PROBE = 100 + """[SERVER] Requests the identity of a board. Expects an ID_CONFIRM response""" PING = 101 + """[SERVER] Attempts to ping a board. Expects a PONG response""" ACKNOWLEDGE = 102 + """[SERVER] Acknowledges the IDENT packet sent by boards. Expects no response""" REASSIGN = 103 + """[SERVER] Reassigns a board's id. Expects no response""" TERMINATE = 104 + """[SERVER] Terminates the currently running job. Eventually expects a READY""" CYCLE = 105 + """[SERVER] Requests a power cycle of a board. Eventually expects a READY""" JOB = 106 + """[SERVER] Sends a job to a testing rack. Eventually expects a JOB_UPDATE""" # Misc packets RAW = 200 + """[MISC] A completely raw datapacket""" class DataPacket: """ @@ -115,9 +134,11 @@ def serialize(self) -> str: return string_construct def __len__(self) -> int: + """Returns the length of the serialized string for this packet""" return len(self.serialize()) def __str__(self) -> str: + """Creates a human-readable version of this packet""" if self.use_raw: if(len(self.raw_data) > 50): return " " + str(self.data) + " [raw: +" + str(len(self.raw_data)) + "c]" @@ -213,18 +234,29 @@ def clear_input_buffer(self): class JobData: + """A struct-like class that describes a single job to be run on the datastreamer""" class GitPullType(int, Enum): + """Describes what kind of data to pull from the remote""" BRANCH = 0 # Pull a branch + """Pull a specific branch from github""" COMMIT = 1 # Pull a specific commit + """Pull a specific commit from github""" class JobType(int, Enum): + """Describes what kind of job the datastreamer will run""" DEFAULT = 0 # Pull code, flash, run hilsim, return result + """Normal job""" DIRTY = 1 # Run hilsim with whatever is currently flashed, return result (tbd) + """Not implemented""" TEST = 2 # Run some sort of test suite (tbd) + """Not implemented""" class JobPriority(int, Enum): + """What priority should this job have with regards to other jobs?""" NORMAL = 0 # Normal priority, goes through queue like usual + """A normal priority job""" HIGH = 1 # High priority, get priority in the queue + """A high priority job""" job_id:int @@ -247,6 +279,7 @@ def __init__(self, job_id, pull_type: GitPullType=GitPullType.BRANCH, pull_targe self.job_timestep = job_timestep def to_dict(self) -> dict: + """Converts this object to a dictionary for easy JSON serialization""" return {'job_id': self.job_id, 'pull_type': self.pull_type, 'pull_target': self.pull_target, 'job_type': self.job_type, 'job_author_id': self.job_author_id, 'job_priority': self.job_priority, 'job_timestep': self.job_timestep} @@ -256,14 +289,20 @@ def to_dict(self) -> dict: class JobStatus: """Static class for making job statuses""" class JobState(int, Enum): + """Enum storing the job state as an integer""" IDLE = 0 + """The job is waiting on something that is blocking it from running""" ERROR = 1 + """The job has errored and cannot continue""" SETUP = 2 + """The job is being set up""" RUNNING = 3 + """The job is actively running and streaming data""" job_state: JobState = JobState.IDLE current_action: str = "" status_text: str = "" + def __init__(self, job_state: JobState, current_action:str, status_text:str) -> None: self.job_state = job_state self.current_action = current_action @@ -273,12 +312,13 @@ def to_dict(self) -> dict: return {'job_state': self.job_state, 'current_action': self.current_action, 'status_text': self.status_text} class HeartbeatServerStatus: - server_state: Enum # ServerState, from main. + """Class representing a server connection test/status update""" + server_state = None # ServerState, from main. server_startup_time: float # (Time.time()) is_busy: bool # Current job running, being set up, or in cleanup. - is_ready: bool # Ready for another job + is_ready: bool # Ready for another job? - def __init__(self, server_state: Enum, server_startup_time: float, + def __init__(self, server_state, server_startup_time: float, is_busy: bool, is_ready: bool) -> None: self.server_state = server_state self.server_startup_time = server_startup_time @@ -286,11 +326,13 @@ def __init__(self, server_state: Enum, server_startup_time: float, self.is_ready = is_ready def to_dict(self) -> dict: + """Converts the heartbeat update into a dictionary so it can be serialized to JSON""" return {'server_state': self.server_state, 'server_startup_time': self.server_startup_time, 'is_busy': self.is_busy, 'is_ready': self.is_ready} class HeartbeatAvionicsStatus: + """Class representing a status update for the avionics system. TBD.""" connected:bool # Connected to server avionics_type:str # May turn this into an enum # More debug info to come @@ -300,6 +342,7 @@ def __init__(self, connected:bool, avionics_type:str) -> None: self.avionics_type = avionics_type def to_dict(self): + """Converts the heartbeat update into a dictionary so it can be serialized to JSON""" return {'connected': self.connected, 'avionics_type':self.avionics_type} @@ -348,7 +391,7 @@ def CL_JOB_UPDATE(job_status: JobStatus, current_log: str) -> DataPacket: @current_log: Current outputs of HILSIM """ packet_data = {'job_status': job_status.to_dict()} - return DataPacket(DataPacketType.BUSY, packet_data, current_log) + return DataPacket(DataPacketType.JOB_UPDATE, packet_data, current_log) def CL_PONG() -> DataPacket: """Constructs PONG packet""" @@ -401,13 +444,16 @@ def SV_JOB(job_data: JobData, flight_csv: str) -> DataPacket: return DataPacket(DataPacketType.JOB, packet_data, flight_csv) class PacketValidator: - def is_server_packet(packet: DataPacket): + def is_server_packet(packet: DataPacket) -> bool: + """Determines whether this packet is a server packet or not. Returns FALSE for misc packets.""" return packet.packet_type.value > 99 and packet.packet_type.value < 199 def is_client_packet(packet: DataPacket): + """Determines whether this packet is a client packet or not. Returns FALSE for misc packets.""" return packet.packet_type.value > -1 and packet.packet_type.value < 99 def validate_server_packet(server_packet: DataPacket): + """Returns whether a given packet is a valid server packet. Will return false for non-server packets""" match server_packet.packet_type: case DataPacketType.IDENT_PROBE: return True @@ -423,8 +469,10 @@ def validate_server_packet(server_packet: DataPacket): return "job_data" in server_packet.data and server_packet.use_raw case DataPacketType.PING: return True + return False - def validate_client_packet(client_packet: DataPacket): + def validate_client_packet(client_packet: DataPacket) -> bool: + """Returns whether a given packet is a valid client packet. Will return false for non-client packets""" match client_packet.packet_type: case DataPacketType.IDENT: return "board_type" in client_packet.data @@ -441,4 +489,5 @@ def validate_client_packet(client_packet: DataPacket): case DataPacketType.BUSY: return "job_data" in client_packet.data case DataPacketType.PONG: - return True \ No newline at end of file + return True + return False \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/pio_commands.py b/Test-Rack-Software/TARS-Rack/util/pio_commands.py index 315333e..ebfec41 100644 --- a/Test-Rack-Software/TARS-Rack/util/pio_commands.py +++ b/Test-Rack-Software/TARS-Rack/util/pio_commands.py @@ -9,6 +9,7 @@ #### Helper functions #### def run_script(arg_list): + """Runs a platformio command in a python subprocess""" print("(pio_commands) Running script [platformio " + str(arg_list) + "]") working_dir = config.platformio_path args = ['platformio'] @@ -19,18 +20,21 @@ def run_script(arg_list): print("(pio_commands) Done.") def pio_build(build_target=None): + """Shortcut for the `build` command in platformio""" if(build_target == None): run_script(['run']) else: run_script(['run', '--environment', build_target]) def pio_upload(build_target=None): + """Shortcut for the `upload` command in platformio, used to flash code.""" if(build_target == None): run_script(['run', '--target', 'upload']) else: run_script(['run', '--target', 'upload', '--environment', build_target]) def pio_clean(build_target=None): + """Shortcut for `build clean` in platformio.""" if(build_target == None): run_script(['run', '--target', 'clean', '-s']) else: diff --git a/Test-Rack-Software/TARS-Rack/util/remote_command.py b/Test-Rack-Software/TARS-Rack/util/remote_command.py index a8d3099..f6afc80 100644 --- a/Test-Rack-Software/TARS-Rack/util/remote_command.py +++ b/Test-Rack-Software/TARS-Rack/util/remote_command.py @@ -11,17 +11,18 @@ import sys import os, shutil -""" -Clone the repository defined in config into the directory defined in config. (Usually ./remote) -""" def clone_repo(): + """ + Clone the repository defined in config into the directory defined in config. (Usually ./remote) + """ print("(git_commands) Cloning repository..") Repo.clone_from(config.repository_url, config.remote_path) -""" -Reset the repository back to its "master" or "main" state by stashing current changes, switching to main, then pulling. -""" + def reset_repo(): + """ + Reset the repository back to its "master" or "main" state by stashing current changes, switching to main, then pulling. + """ repo = Repo(config.remote_path) print("(git_commands) Stashing changes..") repo.git.checkout(".") @@ -29,11 +30,12 @@ def reset_repo(): repo.git.checkout("master") repo.git.pull() -""" -Switch to a specific branch and pull it -@param branch The branch to pull from the remote defined in config. -""" + def pull_branch(branch): + """ + Switch to a specific branch and pull it + @param branch The branch to pull from the remote defined in config. + """ repo = Repo(config.remote_path) print("(git_commands) Fetching repository data") repo.git.fetch() diff --git a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py index ddca0ee..6351d48 100644 --- a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py +++ b/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py @@ -1,50 +1,49 @@ +# This script provides a few wrapper functions for PySerial import serial # Pyserial! Not serial import serial.tools.list_ports -import util.config import util.packets as packet -import json connected_comports: list[serial.Serial] = [] -packet_buffer: dict = {} -""" -Function to retrieve a list of comports connected to this device -@returns ListPortInfo[] A list of port data. -""" + def get_com_ports(): + """ + Function to retrieve a list of comports connected to this device + @returns ListPortInfo[] A list of port data. + """ return serial.tools.list_ports.comports() -"""Close all connected COMports""" def close_com_ports(): + """Close all connected COMports""" print("(cloase_com_ports) Closing all initialized comports..") for port in connected_comports: port.close() def close_port(port: serial.Serial): - print("(cloase_com_ports) Closing " + port.name) - for comport in connected_comports: - if(comport.name == port.name): - comport.close() + """Close a specific port""" + port.close() -"""Clears all of the data in the port buffer""" def clear_port(port: serial.Serial): + """Clears all of the data in the port buffer""" port.reset_input_buffer() port.reset_output_buffer() print("(clear_port) Successfully cleared port " + port.name) -"""Clears all of the data in the port buffer""" + def hard_reset(port: serial.Serial): + """Clears all of the data in the port buffer by closing the port and opening it back up""" port.close() port.open() print("(clear_port) Successfully hard reset port " + port.name) -""" -TODO: Delete this! -Test script for init_com_ports() -""" + alr_init = False def t_init_com_ports(): + """ + TODO: Delete this! + Test script for init_com_ports() + """ global alr_init init_com_ports() if alr_init == False: @@ -54,10 +53,11 @@ def t_init_com_ports(): print("(init_comports) Initialized port COM8") -""" -Loop through each port and try to initialize it if it's not already initialized -""" + def init_com_ports(): + """ + Loop through each port and try to initialize it if it's not already initialized + """ print("(init_comports) Attempting to initialize all connected COM ports..") for port_data in get_com_ports(): try: From 1c4f8620ace523ef4bff292199ef9bf19d8d45fe Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Tue, 24 Oct 2023 16:29:22 -0500 Subject: [PATCH 12/24] Small packet fix and big hilsim feature --- .../API/services/datastreamer_test.py | 95 ++++++++++--------- Central-Server/API/services/serial_tester.py | 37 +++++++- Central-Server/API/util/packets.py | 75 +++++++++++++-- Test-Rack-Software/TARS-Rack/util/config.py | 9 -- Test-Rack-Software/config.py | 36 +++++++ Test-Rack-Software/{TARS-Rack => }/main.py | 83 ++++++++++++---- .../{TARS-Rack => tars_rack}/.gitignore | 0 .../{TARS-Rack => tars_rack}/README.md | 0 .../av_platform/README.md | 0 .../av_platform/csv_datastream.py | 2 +- .../av_platform/flight_computer.csv | 0 .../av_platform/hilsimpacket_pb2.py | 0 .../av_platform/stream_data.py | 0 .../av_platform => tars_rack}/interface.py | 48 +++++++--- Test-Rack-Software/tars_rack/platform_meta.py | 11 +++ .../{TARS-Rack => }/util/README.md | 0 .../util/avionics_interface.py | 17 +++- Test-Rack-Software/util/avionics_meta.py | 16 ++++ .../util/datastreamer_server.py | 62 ++++++++---- .../{TARS-Rack => }/util/git_commands.py | 12 ++- .../{TARS-Rack => }/util/handle_jobs.py | 33 ++++++- .../{TARS-Rack => }/util/handle_packets.py | 13 ++- .../{TARS-Rack => }/util/packets.py | 14 ++- .../{TARS-Rack => }/util/pio_commands.py | 11 ++- .../{TARS-Rack => }/util/remote_command.py | 21 +++- .../{TARS-Rack => }/util/serial_wrapper.py | 0 26 files changed, 462 insertions(+), 133 deletions(-) delete mode 100644 Test-Rack-Software/TARS-Rack/util/config.py create mode 100644 Test-Rack-Software/config.py rename Test-Rack-Software/{TARS-Rack => }/main.py (67%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/.gitignore (100%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/README.md (100%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/av_platform/README.md (100%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/av_platform/csv_datastream.py (93%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/av_platform/flight_computer.csv (100%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/av_platform/hilsimpacket_pb2.py (100%) rename Test-Rack-Software/{TARS-Rack => tars_rack}/av_platform/stream_data.py (100%) rename Test-Rack-Software/{TARS-Rack/av_platform => tars_rack}/interface.py (87%) create mode 100644 Test-Rack-Software/tars_rack/platform_meta.py rename Test-Rack-Software/{TARS-Rack => }/util/README.md (100%) rename Test-Rack-Software/{TARS-Rack => }/util/avionics_interface.py (87%) create mode 100644 Test-Rack-Software/util/avionics_meta.py rename Test-Rack-Software/{TARS-Rack => }/util/datastreamer_server.py (84%) rename Test-Rack-Software/{TARS-Rack => }/util/git_commands.py (71%) rename Test-Rack-Software/{TARS-Rack => }/util/handle_jobs.py (73%) rename Test-Rack-Software/{TARS-Rack => }/util/handle_packets.py (79%) rename Test-Rack-Software/{TARS-Rack => }/util/packets.py (98%) rename Test-Rack-Software/{TARS-Rack => }/util/pio_commands.py (91%) rename Test-Rack-Software/{TARS-Rack => }/util/remote_command.py (84%) rename Test-Rack-Software/{TARS-Rack => }/util/serial_wrapper.py (100%) diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index 9d01c13..e9c71bb 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -19,7 +19,12 @@ if __name__ == "__main__": - + # Get job data + job_data = pkt.JobData(0) + + # Open csv file + file = open(os.path.join(os.path.dirname(__file__), "./datastreamer_test_data.csv"), 'r') + csv_data = file.read() print("Detected script running as __main__, beginning test of Data Streamer functionality") serial_tester.SECTION("Test setup") @@ -32,8 +37,8 @@ serial_tester.CLEAR(port) serial_tester.TRY_WRITE(port, pkt.SV_ACKNOWLEDGE(ack_test_boardid), "Writing valid ACK packet") - print(port.out_waiting) - serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.READY, 10) + serial_tester.TEST("Packet after ACK is READY", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.READY)) serial_tester.TRY_WRITE(port, pkt.SV_IDENT_PROBE(), "Writing IDENT? packet after ACK packet") packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.ID_CONFIRM) @@ -83,62 +88,66 @@ res = "ID-CONF correctly returned board ID " + str(reassign_test_boardid) serial_tester.TEST("Connected board returns properly set ID", (cond, res)) - # Jobs - serial_tester.SECTION("Comprehensive JOB Tests") - serial_tester.SECTION("Initializes job and sends updates") + # Job (cancel) + serial_tester.SECTION("Cancel job test") + - # Get job data - job_data = pkt.JobData(0) - - # Open csv file - file = open(os.path.join(os.path.dirname(__file__), "./datastreamer_test_data.csv"), 'r') - csv_data = file.read() serial_tester.TRY_WRITE(port, pkt.SV_JOB(job_data, csv_data), "Writing JOB packet") packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.JOB_UPDATE, 30) - serial_tester.TEST("Packet after valid JOB packet complies to JOB-UPD", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.JOB_UPDATE)) - - job_good = packet.data['job_status']['job_state'] == 2 and packet.data['job_status']['status_text'] == "Accepted" - serial_tester.TEST("Ensure job_state is '2' and job_status is 'Accepted'", (job_good, f"Got job_ok {packet.data['job_status']['job_state']} and job_status '{packet.data['job_status']['status_text']}'")) - - # Check for packets during setup process + job_good = packet.data['job_status']['job_state'] == 2 and packet.data['job_status']['current_action'] == "ACCEPTED" + serial_tester.TEST("Ensure job_state is '2' and current_action is 'ACCEPTED'", (job_good, f"Got job_ok {packet.data['job_status']['job_state']} and current_action '{packet.data['job_status']['current_action']}'")) - # serial_tester.TEST("Responds with any after building job", serial_tester.AWAIT_ANY_RESPONSE(port, 100, "(Waiting for build: This will take a while.)")) - - # Check for ok update after flash - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Packet after expected build complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) - job_good = data['job_status']['job_ok'] == True and data['job_status']['status'] == "Setup Complete" - serial_tester.TEST("Ensure job_ok is True and job_status is 'Setup Complete'", (job_good, f"Got job_ok {data['job_status']['job_ok']} and job_status '{data['job_status']['status']}'")) + + # Check for flash workflow + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.JOB_UPDATE, 30) + serial_tester.TEST("(part 1) Waiting for COMPILE_READY", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.JOB_UPDATE)) + job_good = packet.data['job_status']['job_state'] == 2 and packet.data['job_status']['current_action'] == "COMPILE_READY" + serial_tester.TEST("Ensure job_state is '2' and current_action is 'COMPILE_READY'", (job_good, f"Got job_ok {packet.data['job_status']['job_state']} and current_action '{packet.data['job_status']['current_action']}'")) + + + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.JOB_UPDATE, 100) + serial_tester.TEST("(part 2) Waiting for COMPILED", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.JOB_UPDATE)) + job_good = packet.data['job_status']['job_state'] == 2 and packet.data['job_status']['current_action'] == "COMPILED" + serial_tester.TEST("Ensure job_state is '2' and current_action is 'COMPILED'", (job_good, f"Got job_ok {packet.data['job_status']['job_state']} and current_action '{packet.data['job_status']['current_action']}'")) # Job updates - serial_tester.TEST("Job updates are sent", serial_tester.AWAIT_ANY_RESPONSE(port, 10, "(Waiting for any job update)")) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Packet after expected run start complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) - job_good = data['job_status']['job_ok'] == True - serial_tester.TEST("Ensure job_ok is True", (job_good, f"Got job_ok {data['job_status']['job_ok']}")) + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.JOB_UPDATE, 10) + serial_tester.TEST("(part 3) Waiting for BEGIN", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.JOB_UPDATE)) + job_good = packet.data['job_status']['job_state'] == 3 and (packet.data['job_status']['current_action'] == "BEGIN" or packet.data['job_status']['current_action'] == "RUNNING") + serial_tester.TEST("Ensure job_ok is True", (job_good, f"Got job_ok {packet.data['job_status']['job_state']}")) # Terminate serial_tester.SECTION("Terminates gracefully and successfully") - time.sleep(0.5) + time.sleep(1) serial_tester.CLEAR(port) - serial_tester.TRY_WRITE(port, pkt.construct_terminate().encode(), "Writing TERMINATE packet") - serial_tester.TEST("TERMINATE response sent (job packet)", serial_tester.AWAIT_ANY_RESPONSE(port, 3, "(Waiting for job update)")) - valid, type, data = serial_tester.GET_PACKET(port) - - serial_tester.TEST("Packet after expected run force stop complies to JOB-UPD", serial_tester.VALID_PACKET(port, "JOB-UPD", valid, type, data)) - job_good = data['job_status']['job_ok'] == False - job_end_correct_reason = data['job_status']['current_action'] == "Stopped" - serial_tester.TEST("Ensure job_ok is False", (job_good, f"Got job_ok {data['job_status']['job_ok']}")) - serial_tester.TEST("Ensure current_action is 'Stopped' (Non-error stop code)", (job_end_correct_reason, f"Got current_action {data['job_status']['current_action']}")) + serial_tester.TRY_WRITE(port, pkt.SV_TERMINATE(), "Writing TERMINATE packet") # Next packet READY? - serial_tester.TEST("Await a READY packet", serial_tester.AWAIT_ANY_RESPONSE(port, 3)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Packet after expected run force stop complies to READY", serial_tester.VALID_PACKET(port, "READY", valid, type, data)) + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.READY, 10) + serial_tester.TEST("Packet after terminate is READY", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.READY)) + # Cycle test + serial_tester.SECTION("Test of cycle") + serial_tester.TRY_WRITE(port, pkt.SV_CYCLE(), "Writing CYCLE packet") + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.READY, 15) + serial_tester.TEST("Packet after cycle is READY", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.READY)) + + # Actual job finish + serial_tester.SECTION("Full job run (no intermediate tests)") + + # Simulated cooldown period + time.sleep(5) + + serial_tester.TRY_WRITE(port, pkt.SV_JOB(job_data, csv_data), "Writing JOB packet") + + + + packet = serial_tester.WAIT_FOR_PACKET_TYPE(port, pkt.DataPacketType.DONE, 180) # Should finish in 3 min ideally + serial_tester.TEST("Job finishes", serial_tester.VALID_PACKET(port, packet, pkt.DataPacketType.DONE)) + print(packet.raw_data) # CLEANUP serial_tester.SECTION("Cleanup") diff --git a/Central-Server/API/services/serial_tester.py b/Central-Server/API/services/serial_tester.py index d9cdcb3..e8cd5a4 100644 --- a/Central-Server/API/services/serial_tester.py +++ b/Central-Server/API/services/serial_tester.py @@ -106,7 +106,9 @@ def ENSURE_NO_RESPONSE(port: serial.Serial, timeout=3.0): start_time = time.time() while(time.time() - start_time < timeout): if port.in_waiting: - return False, "ENSURE_NO_RESPONSE recieved a response." + data = port.read_all() + string = data.decode("utf8") + return False, "ENSURE_NO_RESPONSE recieved a response: " + string return True, "ENSURE_NO_RESPONSE succeeded, no data was transferred." except: return False, "ENSURE_NO_RESPONSE ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m" @@ -137,7 +139,38 @@ def GET_PACKETS(port: serial.Serial, ignore_heartbeat=True, silent=False) -> lis return new_list except: - FAIL("Read:" + port.name, "GET_PACKET ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") + FAIL("Read:" + port.name, "GET_PACKETS ran into a non-recoverable error:\033[90m\n\n" + traceback.format_exc() + "\033[0m") + +def ACCUMULATE_PACKETS(port: serial.Serial, stop_packet_type: pkt.DataPacketType, timeout=3.0): + wait_text(port.name, "Hooking into packet buffer (reading all packets)") + try: + all_packets = [] + start_time = time.time() + while(time.time() - start_time < timeout): + p_in_buffer = GET_PACKETS(port, False, True) + should_stop = False + for p in p_in_buffer: + all_packets.append(p) + if(p.packet_type == stop_packet_type): + should_stop = True + + if should_stop: + PASS("Collect packets (stop)", "(stop packet reached) Collected " + str(len(all_packets)) + " packets from buffer") + return all_packets + if(len(all_packets) > 0): + PASS("Collect packets", "Collected " + str(len(all_packets)) + " packets from buffer") + return all_packets + else: + FAIL("Collect packets", "Serial timed out while waiting for packets") + except Exception as e: + print(e) + FAIL("Wait for packet", "Wait for packet Encountered a non-recoverable error") + +def WAIT_FOR_PACKET_TYPE_ACCUM(packet_accum: list[pkt.DataPacket], packet_type: pkt.DataPacketType): + for packet in packet_accum: + if(packet.packet_type == packet_type): + return packet + FAIL("Wait for packet", "No packet of type " + str(packet_type) + " found in buffer") def WAIT_FOR_PACKET_TYPE(port: serial.Serial, packet_type: pkt.DataPacketType, timeout=3.0): wait_text(port.name, "Waiting for packet of type " + str(packet_type)) diff --git a/Central-Server/API/util/packets.py b/Central-Server/API/util/packets.py index 3082878..21dccca 100644 --- a/Central-Server/API/util/packets.py +++ b/Central-Server/API/util/packets.py @@ -12,29 +12,49 @@ def __init__(self, packet_string, reason="Generic deserialization error", messag self.message = message self.reason = reason if len(self.packet_string) > 100: + # If a packet string is over 100 characters, we truncate it self.packet_string = self.packet_string[0:99] + ".. (rest hidden)" super().__init__(self.message + " " + packet_string + " ==> " + self.reason) class DataPacketType(int, Enum): + """Enum describing the different types of packets we can send/recieve""" IDENT = 0 + """[CLIENT] Identification packet. Sent to servers to identify this as a testing rack.""" ID_CONFIRM = 1 + """[CLIENT] Identification confirmation. Sent to a server after failure to confirm this rack's identity""" READY = 2 + """[CLIENT] Sent when this board is ready for a new job""" DONE = 3 + """[CLIENT] Sent when a board is done with a job""" INVALID = 4 + """[CLIENT] Sent in response to a [SERVER] packet that's invalid""" BUSY = 5 + """[CLIENT] Sent when the client is currently busy with setup or running, and cannot complete the request""" JOB_UPDATE = 6 + """[CLIENT] Sent intermittently while a job is running""" PONG = 7 + """[CLIENT] Sent in response to a PING packet""" HEARTBEAT = 8 + """[CLIENT] Sent to update the Kamaji server of this board's status""" # Server packets IDENT_PROBE = 100 + """[SERVER] Requests the identity of a board. Expects an ID_CONFIRM response""" PING = 101 + """[SERVER] Attempts to ping a board. Expects a PONG response""" ACKNOWLEDGE = 102 + """[SERVER] Acknowledges the IDENT packet sent by boards. Expects no response""" REASSIGN = 103 + """[SERVER] Reassigns a board's id. Expects no response""" TERMINATE = 104 + """[SERVER] Terminates the currently running job. Eventually expects a READY""" CYCLE = 105 + """[SERVER] Requests a power cycle of a board. Eventually expects a READY""" JOB = 106 + """[SERVER] Sends a job to a testing rack. Eventually expects a JOB_UPDATE""" # Misc packets RAW = 200 + """[MISC] A completely raw datapacket""" + ERR = 201 class DataPacket: """ @@ -115,9 +135,11 @@ def serialize(self) -> str: return string_construct def __len__(self) -> int: + """Returns the length of the serialized string for this packet""" return len(self.serialize()) def __str__(self) -> str: + """Creates a human-readable version of this packet""" if self.use_raw: if(len(self.raw_data) > 50): return " " + str(self.data) + " [raw: +" + str(len(self.raw_data)) + "c]" @@ -145,7 +167,10 @@ def write_buffer_to_serial(self, serial_port: serial.Serial) -> None: Writes the entire packet buffer to serial """ serialized_full: str = self.to_serialized_string() + if(len(self.packet_buffer) > 0): + serialized_full += "[[pkt_end]]" self.packet_buffer = [] + serial_port.write(serialized_full.encode()) @@ -173,6 +198,8 @@ def stream_to_packet_list(stream: str, safe_deserialize:bool=False) -> list[Data serialized_packets = stream.split("[[pkt_end]]") ds_packets: list[DataPacket] = [] for ser_pkt in serialized_packets: + if(ser_pkt == ""): + continue new_packet = DataPacket(DataPacketType.RAW, {}) if(safe_deserialize): new_packet.safe_deserialize(ser_pkt) @@ -213,18 +240,29 @@ def clear_input_buffer(self): class JobData: + """A struct-like class that describes a single job to be run on the datastreamer""" class GitPullType(int, Enum): + """Describes what kind of data to pull from the remote""" BRANCH = 0 # Pull a branch + """Pull a specific branch from github""" COMMIT = 1 # Pull a specific commit + """Pull a specific commit from github""" class JobType(int, Enum): + """Describes what kind of job the datastreamer will run""" DEFAULT = 0 # Pull code, flash, run hilsim, return result + """Normal job""" DIRTY = 1 # Run hilsim with whatever is currently flashed, return result (tbd) + """Not implemented""" TEST = 2 # Run some sort of test suite (tbd) + """Not implemented""" class JobPriority(int, Enum): + """What priority should this job have with regards to other jobs?""" NORMAL = 0 # Normal priority, goes through queue like usual + """A normal priority job""" HIGH = 1 # High priority, get priority in the queue + """A high priority job""" job_id:int @@ -247,6 +285,7 @@ def __init__(self, job_id, pull_type: GitPullType=GitPullType.BRANCH, pull_targe self.job_timestep = job_timestep def to_dict(self) -> dict: + """Converts this object to a dictionary for easy JSON serialization""" return {'job_id': self.job_id, 'pull_type': self.pull_type, 'pull_target': self.pull_target, 'job_type': self.job_type, 'job_author_id': self.job_author_id, 'job_priority': self.job_priority, 'job_timestep': self.job_timestep} @@ -256,14 +295,20 @@ def to_dict(self) -> dict: class JobStatus: """Static class for making job statuses""" class JobState(int, Enum): + """Enum storing the job state as an integer""" IDLE = 0 + """The job is waiting on something that is blocking it from running""" ERROR = 1 + """The job has errored and cannot continue""" SETUP = 2 + """The job is being set up""" RUNNING = 3 + """The job is actively running and streaming data""" job_state: JobState = JobState.IDLE current_action: str = "" status_text: str = "" + def __init__(self, job_state: JobState, current_action:str, status_text:str) -> None: self.job_state = job_state self.current_action = current_action @@ -273,12 +318,13 @@ def to_dict(self) -> dict: return {'job_state': self.job_state, 'current_action': self.current_action, 'status_text': self.status_text} class HeartbeatServerStatus: - server_state: Enum # ServerState, from main. + """Class representing a server connection test/status update""" + server_state = None # ServerState, from main. server_startup_time: float # (Time.time()) is_busy: bool # Current job running, being set up, or in cleanup. - is_ready: bool # Ready for another job + is_ready: bool # Ready for another job? - def __init__(self, server_state: Enum, server_startup_time: float, + def __init__(self, server_state, server_startup_time: float, is_busy: bool, is_ready: bool) -> None: self.server_state = server_state self.server_startup_time = server_startup_time @@ -286,11 +332,13 @@ def __init__(self, server_state: Enum, server_startup_time: float, self.is_ready = is_ready def to_dict(self) -> dict: + """Converts the heartbeat update into a dictionary so it can be serialized to JSON""" return {'server_state': self.server_state, 'server_startup_time': self.server_startup_time, 'is_busy': self.is_busy, 'is_ready': self.is_ready} class HeartbeatAvionicsStatus: + """Class representing a status update for the avionics system. TBD.""" connected:bool # Connected to server avionics_type:str # May turn this into an enum # More debug info to come @@ -300,6 +348,7 @@ def __init__(self, connected:bool, avionics_type:str) -> None: self.avionics_type = avionics_type def to_dict(self): + """Converts the heartbeat update into a dictionary so it can be serialized to JSON""" return {'connected': self.connected, 'avionics_type':self.avionics_type} @@ -328,7 +377,7 @@ def CL_DONE(job_data: JobData, hilsim_result:str) -> DataPacket: @job_data: The job data sent along with this packet (For ID purposes) @hilsim_result: Raw string of the HILSIM output""" packet_data = {'job_data': job_data.to_dict()} - return DataPacket(DataPacketType.READY, packet_data, hilsim_result) + return DataPacket(DataPacketType.DONE, packet_data, hilsim_result) def CL_INVALID(invalid_packet:DataPacket) -> DataPacket: """Constructs INVALID packet (RAW) @@ -400,14 +449,23 @@ def SV_JOB(job_data: JobData, flight_csv: str) -> DataPacket: packet_data = {'job_data': job_data.to_dict()} return DataPacket(DataPacketType.JOB, packet_data, flight_csv) +# Misc packets +def MISC_ERR(error: str) -> DataPacket: + """Constructs ERR packet""" + packet_data = {'error': error} + return DataPacket(DataPacketType.ERR, packet_data) + class PacketValidator: - def is_server_packet(packet: DataPacket): + def is_server_packet(packet: DataPacket) -> bool: + """Determines whether this packet is a server packet or not. Returns FALSE for misc packets.""" return packet.packet_type.value > 99 and packet.packet_type.value < 199 def is_client_packet(packet: DataPacket): + """Determines whether this packet is a client packet or not. Returns FALSE for misc packets.""" return packet.packet_type.value > -1 and packet.packet_type.value < 99 def validate_server_packet(server_packet: DataPacket): + """Returns whether a given packet is a valid server packet. Will return false for non-server packets""" match server_packet.packet_type: case DataPacketType.IDENT_PROBE: return True @@ -423,8 +481,10 @@ def validate_server_packet(server_packet: DataPacket): return "job_data" in server_packet.data and server_packet.use_raw case DataPacketType.PING: return True + return False - def validate_client_packet(client_packet: DataPacket): + def validate_client_packet(client_packet: DataPacket) -> bool: + """Returns whether a given packet is a valid client packet. Will return false for non-client packets""" match client_packet.packet_type: case DataPacketType.IDENT: return "board_type" in client_packet.data @@ -441,4 +501,5 @@ def validate_client_packet(client_packet: DataPacket): case DataPacketType.BUSY: return "job_data" in client_packet.data case DataPacketType.PONG: - return True \ No newline at end of file + return True + return False \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/config.py b/Test-Rack-Software/TARS-Rack/util/config.py deleted file mode 100644 index 273a76e..0000000 --- a/Test-Rack-Software/TARS-Rack/util/config.py +++ /dev/null @@ -1,9 +0,0 @@ -import os - -board_type = "TARS" - -repository_url = "https://github.com/ISSUIUC/TARS-Software.git" -platformio_subdirectory = "./TARS" # Platformio subdirectory (in relation to the repository itself) - -remote_path = os.path.join(os.path.dirname(__file__), "../remote") -platformio_path = os.path.join(remote_path, platformio_subdirectory) \ No newline at end of file diff --git a/Test-Rack-Software/config.py b/Test-Rack-Software/config.py new file mode 100644 index 0000000..36bb116 --- /dev/null +++ b/Test-Rack-Software/config.py @@ -0,0 +1,36 @@ +# This file is not a "traditional" configuration file, it instead provides runtime symbolic links to a +# defined avionics interface. This means that by defining which stack you want to use in the +# next couple of lines, this file automatically tells `main.py` which file to access. + +# Metadata needs to be imported first, so the avionics system knows what data to use for startup. +# Import all metaconfigs +import tars_rack.platform_meta as TARSmkIVmeta + + +# == EDIT THE VARIABLE BELOW TO CHANGE WHICH METADATA IS USED == +use_meta = TARSmkIVmeta.meta +"""Which `platform_meta.py` file should we use?""" +# ============================================================== + + +# Continue by importing all interfaces +import tars_rack.interface as TARSmkIV + +# == EDIT THE VARIABLE BELOW TO CHANGE WHICH INTERFACE IS USED == +use_interface = TARSmkIV +"""Which interface should this testing rack use?""" +# =============================================================== + + + + + + + + + +# ========================= STOP! =========================== +# This is the end of the basic configuration, proceed only if you know what you are doing + +# nothing here lol +print("(config) Configuration loaded") diff --git a/Test-Rack-Software/TARS-Rack/main.py b/Test-Rack-Software/main.py similarity index 67% rename from Test-Rack-Software/TARS-Rack/main.py rename to Test-Rack-Software/main.py index 42c90db..e942ca9 100644 --- a/Test-Rack-Software/TARS-Rack/main.py +++ b/Test-Rack-Software/main.py @@ -9,35 +9,31 @@ # will be to recieve job data and pass it on to whichever device is connected to this device, then send that data back. # That's it! Work of when to run jobs/hardware cooldowns/getting jobs will be handled by the central server. -import av_platform.interface as avionics -import av_platform.stream_data as platform +import config as test_board_config import util.serial_wrapper as connection import util.packets as packet -import util.config as config -import os import time -import serial -from enum import Enum +import util.avionics_meta as AVMeta import util.handle_packets as handle_packets import util.datastreamer_server as Datastreamer +# Set up interface defined in config +avionics = test_board_config.use_interface +av_meta: AVMeta.PlatformMetaInterface = test_board_config.use_meta + ############### HILSIM Data-Streamer-RASPI ############### def handle_error_state(Server: Datastreamer.DatastreamerServer): - pass - -def handle_job_setup_error(Server: Datastreamer.DatastreamerServer): - pass - -def handle_job_runtime_error(Server: Datastreamer.DatastreamerServer): + # Recoverable error pass def handle_first_setup(Server: Datastreamer.DatastreamerServer): try: - Server.board_type = config.board_type + Server.board_type = av_meta.board_type avionics.av_instance.first_setup() return True except Exception as e: + print(e) return False def should_heartbeat(Server: Datastreamer.DatastreamerServer): @@ -51,13 +47,12 @@ def send_wide_ident(Server: Datastreamer.DatastreamerServer): if(time.time() > Server.last_server_connection_check): connection.t_init_com_ports() for port in connection.connected_comports: - packet.DataPacketBuffer.write_packet(packet.CL_IDENT(config.board_type), port) + packet.DataPacketBuffer.write_packet(packet.CL_IDENT(av_meta.board_type), port) Server.last_server_connection_check += 0.25 return True def check_server_connection(Server: Datastreamer.DatastreamerServer): - global board_id, server_port for port in connection.connected_comports: in_packets = packet.DataPacketBuffer.serial_to_packet_list(port, True) for pkt in in_packets: @@ -66,10 +61,11 @@ def check_server_connection(Server: Datastreamer.DatastreamerServer): Server.server_port = port #temporary, close all non-server ports: - for port in connection.connected_comports: + + """for port in connection.connected_comports: if(port.name != Server.server_port.name): print("TEMP: closed " + port.name) - port.close() + port.close()""" return True return False @@ -83,8 +79,49 @@ def reset_connection_data(Server: Datastreamer.DatastreamerServer): return True def detect_avionics(Server: Datastreamer.DatastreamerServer): - # Let's assume we can detect avionics already - return True + try: + return avionics.av_instance.detect() + except Exception as e: + print(e) + + +def on_ready(Server: Datastreamer.DatastreamerServer): + Server.signal_abort = False + + # we only want to send ready packet if we are not in the process of CYCLE'ing! + if(not Server.signal_cycle): + Server.packet_buffer.add(packet.CL_READY()) + print("(transition_to_ready) Reset fail flags and sent READY packet.") + else: + print("(transition_to_ready) In CYCLE process! Cleared fail flag") + +def handle_power_cycle(Server: Datastreamer.DatastreamerServer): + SState = Datastreamer.ServerStateController.ServerState + # This is linked to ANY state, but we realistically only care about some: + ignore_state = [SState.AV_DETECT, SState.CONNECTING, SState.INIT] + if Server.state.server_state in ignore_state: + return + + # We now know we're in a state with connected avionics + if Server.signal_cycle: + # Make sure we wait until all jobs are stopped to avoid weird side effects + if not Server.job_active: + # Now we actually cycle the board + try: + if (avionics.av_instance.power_cycle()): + print("(power_cycle) Successfully power cycled avionics system") + # Manually send READY packet if not in ready state (May not be in READY state!) + Server.packet_buffer.add(packet.CL_READY()) + print("(power_cycle) Sent READY packet") + Server.signal_abort = False + Server.signal_cycle = False + else: + print("Unable to power cycle!") + Server.packet_buffer.add(packet.MISC_ERR("Unable to power cycle")) + except: + print("ERROR during power cycle!") + Server.packet_buffer.add(packet.MISC_ERR("Error during power cycle")) + def main(): Server = Datastreamer.instance @@ -96,8 +133,6 @@ def main(): # FSM error handling: # Generic error Server.state.add_transition_event(SState.ANY, SState.ERROR, handle_error_state) - Server.state.add_transition_event(SState.JOB_SETUP, SState.JOB_ERROR, handle_job_setup_error) - Server.state.add_transition_event(SState.JOB_RUNNING, SState.JOB_ERROR, handle_job_runtime_error) # Set up FSM flow: Server.state.add_transition_pipe(SState.INIT, SState.CONNECTING) # Always try to transition INIT => CONNECTING @@ -114,6 +149,12 @@ def main(): Server.state.add_transition_event(SState.CONNECTING, SState.AV_DETECT, check_server_connection) Server.state.add_transition_event(SState.AV_DETECT, SState.READY, detect_avionics) + # Success events + Server.state.add_success_event(SState.ANY, SState.READY, on_ready) + + # Always events + Server.state.add_always_event(SState.ANY, handle_power_cycle) + handle_packets.add_transitions(Server.state) handle_packets.add_always_events(Server.state) diff --git a/Test-Rack-Software/TARS-Rack/.gitignore b/Test-Rack-Software/tars_rack/.gitignore similarity index 100% rename from Test-Rack-Software/TARS-Rack/.gitignore rename to Test-Rack-Software/tars_rack/.gitignore diff --git a/Test-Rack-Software/TARS-Rack/README.md b/Test-Rack-Software/tars_rack/README.md similarity index 100% rename from Test-Rack-Software/TARS-Rack/README.md rename to Test-Rack-Software/tars_rack/README.md diff --git a/Test-Rack-Software/TARS-Rack/av_platform/README.md b/Test-Rack-Software/tars_rack/av_platform/README.md similarity index 100% rename from Test-Rack-Software/TARS-Rack/av_platform/README.md rename to Test-Rack-Software/tars_rack/av_platform/README.md diff --git a/Test-Rack-Software/TARS-Rack/av_platform/csv_datastream.py b/Test-Rack-Software/tars_rack/av_platform/csv_datastream.py similarity index 93% rename from Test-Rack-Software/TARS-Rack/av_platform/csv_datastream.py rename to Test-Rack-Software/tars_rack/av_platform/csv_datastream.py index 577f787..73df7f3 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/csv_datastream.py +++ b/Test-Rack-Software/tars_rack/av_platform/csv_datastream.py @@ -1,4 +1,4 @@ -import av_platform.hilsimpacket_pb2 as hilsimpacket_pb2 +import tars_rack.av_platform.hilsimpacket_pb2 as hilsimpacket_pb2 def csv_line_to_protobuf(parsed_csv): diff --git a/Test-Rack-Software/TARS-Rack/av_platform/flight_computer.csv b/Test-Rack-Software/tars_rack/av_platform/flight_computer.csv similarity index 100% rename from Test-Rack-Software/TARS-Rack/av_platform/flight_computer.csv rename to Test-Rack-Software/tars_rack/av_platform/flight_computer.csv diff --git a/Test-Rack-Software/TARS-Rack/av_platform/hilsimpacket_pb2.py b/Test-Rack-Software/tars_rack/av_platform/hilsimpacket_pb2.py similarity index 100% rename from Test-Rack-Software/TARS-Rack/av_platform/hilsimpacket_pb2.py rename to Test-Rack-Software/tars_rack/av_platform/hilsimpacket_pb2.py diff --git a/Test-Rack-Software/TARS-Rack/av_platform/stream_data.py b/Test-Rack-Software/tars_rack/av_platform/stream_data.py similarity index 100% rename from Test-Rack-Software/TARS-Rack/av_platform/stream_data.py rename to Test-Rack-Software/tars_rack/av_platform/stream_data.py diff --git a/Test-Rack-Software/TARS-Rack/av_platform/interface.py b/Test-Rack-Software/tars_rack/interface.py similarity index 87% rename from Test-Rack-Software/TARS-Rack/av_platform/interface.py rename to Test-Rack-Software/tars_rack/interface.py index 19c2233..64f1492 100644 --- a/Test-Rack-Software/TARS-Rack/av_platform/interface.py +++ b/Test-Rack-Software/tars_rack/interface.py @@ -9,7 +9,7 @@ import util.git_commands as git import util.pio_commands as pio import util.serial_wrapper as server -import av_platform.csv_datastream as csv_datastream +import tars_rack.av_platform.csv_datastream as csv_datastream import pandas import io import time @@ -28,18 +28,21 @@ def handle_init(self) -> None: def detect(self) -> bool: # For TARS, we need to make sure that we're already connected to the server - if(not self.server): - self.ready = False - return print("(detect_avionics) Attempting to detect avionics") + if(not self.server.server_port): + print("(detect_avionics) No server detected!") + self.ready = False + return False + ignore_ports = [self.server.server_port] for comport in server.connected_comports: if not (comport in ignore_ports): - print("(detect_avionics) Detected viable target @ " + comport.name) + print("(detect_avionics) Detected viable avionics target @ " + comport.name) self.TARS_port = comport self.ready = True + return True def first_setup(self) -> None: git.remote_clone() @@ -48,7 +51,12 @@ def first_setup(self) -> None: def code_reset(self) -> None: git.remote_reset() # Clean build dir - pio.pio_clean() + # TODO: add line below back, removed for faster compilation in testing. + # pio.pio_clean() + + def power_cycle(self) -> bool: + # Unfortunately TARS doesn't support power cycling :( + return True def code_pull(self, git_target) -> None: git.remote_pull_branch(git_target) @@ -59,6 +67,7 @@ def code_flash(self) -> None: try: pio.pio_upload("mcu_hilsim") except: + print("(code_flash) First flash failed, attempting re-flash for Teensy error 1") pio.pio_upload("mcu_hilsim") class HilsimRun(AVInterface.HilsimRunInterface): @@ -75,24 +84,25 @@ class HilsimRun(AVInterface.HilsimRunInterface): job_data = None def get_current_log(self) -> str: - return self.return_log + return "\n".join(self.return_log) def job_setup(self): + print("ABORT?", self.server.signal_abort) if (self.job == None): raise Exception("Setup error: Server.current_job is not defined.") # Temporarily close port so code can flash - # self.av_interface.TARS_port.close() + self.av_interface.TARS_port.close() + print("(job_setup) deferred TARS port control to platformio") if(self.job.pull_type == pkt.JobData.GitPullType.BRANCH): try: - self.av_interface.code_reset() # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) self.server.defer() if(self.server.signal_abort): - self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) return False, "Abort signal recieved" self.av_interface.code_pull(self.job.pull_target) @@ -103,7 +113,7 @@ def job_setup(self): # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) self.server.defer() if(self.server.signal_abort): - self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) return False, "Abort signal recieved" self.av_interface.code_flash() @@ -115,7 +125,7 @@ def job_setup(self): # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) self.server.defer() if(self.server.signal_abort): - self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) return False, "Abort signal recieved" # Wait for the port to open back up (Max wait 10s) @@ -124,7 +134,7 @@ def job_setup(self): # Check for defer (This may be DRY, but there aren't many better ways to do this --MK) self.server.defer() if(self.server.signal_abort): - self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.CLEANUP) + self.server.state.try_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) return False, "Abort signal recieved" try: @@ -137,6 +147,7 @@ def job_setup(self): return False, "Unable to re-open avionics COM Port" except Exception as e: + print(e) return False, "Setup failed: " + str(e) elif (self.job.pull_type == pkt.JobData.GitPullType.COMMIT): raise NotImplementedError("Commit-based pulls are not implemented yet.") @@ -164,10 +175,15 @@ def __init__(self, datastreamer: Datastreamer.DatastreamerServer, av_interface: self.job_data = job def reset_clock(self): + """Resets start time to current time""" + print("clock reset") self.start_time = time.time() self.current_time = self.start_time self.last_packet_time = self.start_time + def post_setup(self) -> None: + self.reset_clock() + """ Runs one iteration of the HILSIM loop, with a change in time of dt. @@ -180,12 +196,12 @@ def step(self, dt: float): # can safely send data. if self.current_time > self.last_packet_time + simulation_dt: self.last_packet_time += simulation_dt - if self.current_time < self.start_time + 5: # Wait for 5 seconds to make sure serial is connected pass else: if(self.current_line == 0): + self.last_packet_time = self.current_time job_status = pkt.JobStatus(pkt.JobStatus.JobState.RUNNING, "BEGIN", f"Running (Data streaming started)") status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) self.av_interface.server.packet_buffer.add(status_packet) @@ -197,6 +213,8 @@ def step(self, dt: float): status_packet: pkt.DataPacket = pkt.CL_JOB_UPDATE(job_status, "\n".join(self.return_log)) self.av_interface.server.packet_buffer.add(status_packet) + print(f"Running ({self.current_line/len(self.flight_data_dataframe)*100:.2f}%) [{self.current_line} processed out of {len(self.flight_data_dataframe)} total]") + line_num, row = next(self.flight_data_rows, (None, None)) if line_num == None: job_status = pkt.JobStatus(pkt.JobStatus.JobState.RUNNING, "RUNNING", f"Finished data streaming") @@ -225,6 +243,6 @@ def step(self, dt: float): self.return_log.append(string) return False, False, self.return_log - + av_instance = TARSAvionics(Datastreamer.instance) \ No newline at end of file diff --git a/Test-Rack-Software/tars_rack/platform_meta.py b/Test-Rack-Software/tars_rack/platform_meta.py new file mode 100644 index 0000000..18fb6d8 --- /dev/null +++ b/Test-Rack-Software/tars_rack/platform_meta.py @@ -0,0 +1,11 @@ +import util.avionics_meta as AVMeta + +class PlatformMeta(AVMeta.PlatformMetaInterface): + board_type = "TARSmkIV" + repository_url = "https://github.com/ISSUIUC/TARS-Software.git" + platformio_subdirectory = "./TARS" # Platformio subdirectory (in relation to the repository itself) + + def __init__(self, file: str) -> None: + super().__init__(file) + +meta = PlatformMeta(__file__) diff --git a/Test-Rack-Software/TARS-Rack/util/README.md b/Test-Rack-Software/util/README.md similarity index 100% rename from Test-Rack-Software/TARS-Rack/util/README.md rename to Test-Rack-Software/util/README.md diff --git a/Test-Rack-Software/TARS-Rack/util/avionics_interface.py b/Test-Rack-Software/util/avionics_interface.py similarity index 87% rename from Test-Rack-Software/TARS-Rack/util/avionics_interface.py rename to Test-Rack-Software/util/avionics_interface.py index 2ad51af..fd6c1d0 100644 --- a/Test-Rack-Software/TARS-Rack/util/avionics_interface.py +++ b/Test-Rack-Software/util/avionics_interface.py @@ -1,6 +1,7 @@ import util.datastreamer_server as Datastreamer import util.packets as pkt from abc import ABC, abstractmethod # ABC = Abstract Base Classes +import os class AvionicsInterface(ABC): @@ -10,7 +11,7 @@ class AvionicsInterface(ABC): server: Datastreamer.DatastreamerServer = None def __init__(self, datastreamer: Datastreamer.DatastreamerServer) -> None: - server = datastreamer + self.server = datastreamer @abstractmethod def handle_init(self) -> None: @@ -44,6 +45,15 @@ def code_flash(self) -> None: """Flashes currently staged code to the AV stack""" raise NotImplementedError("AvionicsInterface.code_flash method not implemented") + + @abstractmethod + def power_cycle(self) -> bool: + """Attempt to power cycle the avionics stack. Make sure to call server.defer() if it's a blocking action + + @returns whether power cycling was successful""" + raise NotImplementedError("AvionicsInterface.HilsimRun.power_cycle method not implemented") + + class HilsimRunInterface(ABC): """The server to defer to""" server: Datastreamer.DatastreamerServer = None @@ -77,6 +87,10 @@ def __init__(self, datastreamer: Datastreamer.DatastreamerServer, av_interface: self.av_interface = av_interface self.job = job self.server = datastreamer + + def post_setup(self) -> None: + """Dictates code that runs after setup is complete. Not required.""" + pass @abstractmethod def step(self, dt: float, send_status) -> tuple[bool, bool, str]: @@ -86,5 +100,4 @@ def step(self, dt: float, send_status) -> tuple[bool, bool, str]: @Returns a tuple: (run_finished, run_errored, return_log) """ raise NotImplementedError("AvionicsInterface.HilsimRun.step method not implemented") - diff --git a/Test-Rack-Software/util/avionics_meta.py b/Test-Rack-Software/util/avionics_meta.py new file mode 100644 index 0000000..491b723 --- /dev/null +++ b/Test-Rack-Software/util/avionics_meta.py @@ -0,0 +1,16 @@ +# Class for `platform_meta.py` files. +import os + +class PlatformMetaInterface: + board_type: str = "" + """What type of avionics stack does this interface work with?""" + repository_url: str = "" + """GitHub URL to the repository for this stack""" + platformio_subdirectory = "" # Platformio subdirectory (in relation to the repository itself) + """The subdirectory of the platformio directory, in relation to the repository itself""" + + def __init__(self, file: str) -> None: + self.remote_path = os.path.join(os.path.dirname(file), './remote') + """What path should the `remote` folder be in?""" + self.platformio_path = os.path.join(self.remote_path, self.platformio_subdirectory) + """Platformio path, calculated from remote path""" \ No newline at end of file diff --git a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py b/Test-Rack-Software/util/datastreamer_server.py similarity index 84% rename from Test-Rack-Software/TARS-Rack/util/datastreamer_server.py rename to Test-Rack-Software/util/datastreamer_server.py index dcef812..6d786a4 100644 --- a/Test-Rack-Software/TARS-Rack/util/datastreamer_server.py +++ b/Test-Rack-Software/util/datastreamer_server.py @@ -5,12 +5,13 @@ sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) -import av_platform.interface as avionics import util.packets as packet import time import serial from enum import Enum + + class ServerStateController(): """A class representing the server's state as a state machine. @@ -18,7 +19,8 @@ class ServerStateController(): @`Pipes (A=>B)` are transitions (to state B) that the server is constantly attempting to perform if it's in state A @`Transition events` are blocks of code that are run when specific transitions are performed. If using `try_transition`, they can also dictate whether the transition succeeds. - @`Always events` are blocks of code that are executed each tick when on a specific state""" + @`Always events` are blocks of code that are executed each tick when on a specific state + @`Success events` are identical to transition events, but are only executed once, when the transition succeeds.""" server = None class ServerState(int, Enum): "Enum describing possible server states as an integer." @@ -74,22 +76,24 @@ def __init__(self, server, initial_state, final_state, callback) -> None: self.state_b: ServerStateController.ServerState = final_state self.transition_callback = callback - def run(self, state_a, state_b): + def run(self): + """Execute the transition callback""" + return self.transition_callback(self.server) + + def should_run(self, state_a, state_b): + """Determine if a callback should run""" if(state_a == self.state_a and self.state_b == ServerStateController.ServerState.ANY): - # If transition A -> ANY - return self.transition_callback(self.server) + return True if(state_b == self.state_b and self.state_a == ServerStateController.ServerState.ANY): - # If transition ANY -> B - return self.transition_callback(self.server) + return True if(state_a == self.state_a and state_b == self.state_b): - # If transition A -> B - return self.transition_callback(self.server) - return True - + return True + return False transition_events: list[StateTransition] = [] + success_events: list[StateTransition] = [] transition_always: list[Always] = [] error_transition_events: list[StateTransition] = [] server_state: ServerState = ServerState.INIT @@ -103,6 +107,10 @@ def add_transition_event(self, initial_state, final_state, callback) -> None: """Add a `transition_event` to the state machine""" self.transition_events.append(ServerStateController.StateTransition(self.server, initial_state, final_state, callback)) + def add_success_event(self, initial_state, final_state, callback) -> None: + """Add a `success_event` to the state machine""" + self.success_events.append(ServerStateController.StateTransition(self.server, initial_state, final_state, callback)) + def add_always_event(self, always_target, callback) -> None: """Add an `always_event` to the state machine""" self.transition_always.append(ServerStateController.Always(self.server, always_target, callback)) @@ -113,10 +121,17 @@ def force_transition(self, to_state: ServerState) -> None: for transition_event in self.transition_events: # For each transition event, we check if its to_state matches the state we're forcing the transition to. # If yes, we execute the code but discard the result. - if(transition_event.state_b == to_state or transition_event.state_b == ServerStateController.ServerState.ANY): + if(transition_event.should_run(self.server_state, to_state)): transition_checks += 1 + transition_event.run() + + for success_event in self.success_events: + # Since this transition is forced, we always call all success_events + if(success_event.should_run(self.server_state, to_state)): + transition_checks += 1 + success_event.run() + - transition_event.run(self.server_state, to_state) # Report transition print("(server_state) Successfully transitioned to state", to_state, "(Executed", transition_checks, "transition functions)") @@ -132,17 +147,26 @@ def try_transition(self, to_state: ServerState) -> bool: transition_checks = 0 for transition_event in self.transition_events: # For each transition event, we check if its to_state matches what state we're attempting to transition to. - if(transition_event.state_b == to_state or transition_event.state_b == ServerStateController.ServerState.ANY): + if(transition_event.should_run(self.server_state, to_state)): transition_checks += 1 # If yes, we check if the transition event allows us to transition. + if(not transition_event.run()): + successful_transition = False - #transition_event.run performs its own run checks. - if(not transition_event.run(self.server_state, to_state)): - successful_transition = False + # If all transition checks succeed, we transition to the new state. if successful_transition: print("(server_state) Successfully transitioned to state", to_state, "(Passed", transition_checks, "transition checks)") + success_events_executed = 0 + for success_event in self.success_events: + # Call all success events if the transition succeeds + if(success_event.should_run(self.server_state, to_state)): + success_events_executed += 1 + success_event.run() + + if success_events_executed > 0: + print("(server_state) Executed " + str(success_events_executed) + " success events") self.server_state = to_state return successful_transition @@ -187,6 +211,8 @@ class DatastreamerServer: signal_abort = False """Standin for a process SIGABRT. If set to true, jobs will attempt to stop as soon as it's gracefully possible to do so""" + signal_cycle = False + """Signal to cycle the board when avaliable""" job_active = False """Boolean to check if a job is currently being run""" job_clock_reset = False @@ -210,6 +236,8 @@ def tick(self): This function will generally be run within an infinite while loop.""" if self.server_port != None: # We clear out output buffer and also populate our input buffer from the server + if(len(self.packet_buffer.packet_buffer) > 0): + print(self.packet_buffer.to_serialized_string()) self.packet_buffer.write_buffer_to_serial(self.server_port) self.packet_buffer.read_to_input_buffer(self.server_port) diff --git a/Test-Rack-Software/TARS-Rack/util/git_commands.py b/Test-Rack-Software/util/git_commands.py similarity index 71% rename from Test-Rack-Software/TARS-Rack/util/git_commands.py rename to Test-Rack-Software/util/git_commands.py index 788e51d..34475f1 100644 --- a/Test-Rack-Software/TARS-Rack/util/git_commands.py +++ b/Test-Rack-Software/util/git_commands.py @@ -5,10 +5,16 @@ import subprocess import os -import util.config as config +import config as cfg +import util.avionics_meta + +config: util.avionics_meta.PlatformMetaInterface = cfg.use_meta #### Helper functions #### def run_script(arg_list): + """Runs remote_command.py with the given argument list + @arg_list {List[str]} -- A list of arguments to execute the command with + @description remote_command.py is a wrapper for GitPy, which leaks memory""" print("(git_commands) Running script [python remote-command.py " + str(arg_list) + "]") script_dir = os.path.join(os.path.dirname(__file__), "./remote_command.py") args = ['python', script_dir] @@ -18,12 +24,15 @@ def run_script(arg_list): print("(git_commands) Done.") def remote_clone(): + """Clones the repository defined in the config""" + print("Exists", config.remote_path) if(os.path.exists(config.remote_path)): print("(git_commands) Remote already exists, skipping [remote_clone]!") else: run_script(['clone']) def remote_reset(): + """Resets the remote to `main`""" if(not os.path.exists(config.remote_path)): print("Remote does not exist! Running [remote_command.py clone]") remote_clone() @@ -31,6 +40,7 @@ def remote_reset(): def remote_pull_branch(branch): + """Pulls a specific branch""" if(not os.path.exists(config.remote_path)): print("Remote does not exist! Running [remote_command.py clone]") remote_clone() diff --git a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py b/Test-Rack-Software/util/handle_jobs.py similarity index 73% rename from Test-Rack-Software/TARS-Rack/util/handle_jobs.py rename to Test-Rack-Software/util/handle_jobs.py index d98361f..d89c6dc 100644 --- a/Test-Rack-Software/TARS-Rack/util/handle_jobs.py +++ b/Test-Rack-Software/util/handle_jobs.py @@ -1,22 +1,27 @@ import util.datastreamer_server as Datastreamer import util.packets as pkt -import av_platform.interface as avionics +import config as test_board_config import util.avionics_interface as AVInterface import time +import inspect + +avionics = test_board_config.use_interface def run_setup_job(Server: Datastreamer.DatastreamerServer): """Invokes the avionics system's job setup method. Avionics setup methods are generally blocking. Make sure that they properly call Server.defer() when possible.""" try: + Server.job_active = True job = Server.current_job_data - accepted_status = pkt.JobStatus(pkt.JobStatus.JobState.SETUP, "Setting up job " + str(job.job_id), "Accepted") + accepted_status = pkt.JobStatus(pkt.JobStatus.JobState.SETUP, "ACCEPTED", "Setting up job " + str(job.job_id)) Server.packet_buffer.add(pkt.CL_JOB_UPDATE(accepted_status, "")) Server.defer() current_job: AVInterface.HilsimRunInterface = Server.current_job # For type hints setup_successful, setup_fail_reason = current_job.job_setup() if(setup_successful): + current_job.post_setup() return True else: raise Exception("Setup failed: " + setup_fail_reason) @@ -26,19 +31,36 @@ def run_setup_job(Server: Datastreamer.DatastreamerServer): return False def run_job(Server: Datastreamer.DatastreamerServer): + """Invokes the step() method in the current HilsimRun (plaform-blind)""" dt = time.time() - Server.last_job_step_time - run_finished, run_errored, return_log = Server.current_job.step(dt) + current_job: AVInterface.HilsimRunInterface = Server.current_job # For type hints + + if(Server.signal_abort): + job_status = pkt.JobStatus(pkt.JobStatus.JobState.ERROR, "ABORTED_MANUAL", f"Abort signal was sent") + Server.packet_buffer.add(pkt.CL_JOB_UPDATE(job_status, current_job.get_current_log())) + Server.state.force_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) + return False + + run_finished, run_errored, return_log = current_job.step(dt) if(run_finished): if(run_errored): Server.state.force_transition(Datastreamer.ServerStateController.ServerState.JOB_ERROR) return False else: - # The job has successfully completed! Inform the server of this fact - # TODO: report job finish status to server + print("(job_done) Finished job with job id " + str(Server.current_job_data.job_id)) + Server.packet_buffer.add(pkt.CL_DONE(Server.current_job_data, "\n".join(return_log))) + print("(job_done) sent DONE packet to server with job result") return True Server.last_job_step_time = time.time() + return False + +def job_cleanup(Server: Datastreamer.DatastreamerServer): + Server.job_active = False + Server.current_job = None + Server.current_job_data = None + return True def handle_job_setup_error(Server: Datastreamer.DatastreamerServer): pass @@ -53,6 +75,7 @@ def handle_job_transitions(statemachine: Datastreamer.ServerStateController): statemachine.add_transition_event(SState.JOB_RUNNING, SState.CLEANUP, run_job) statemachine.add_transition_event(SState.JOB_SETUP, SState.JOB_ERROR, handle_job_setup_error) statemachine.add_transition_event(SState.JOB_RUNNING, SState.JOB_ERROR, handle_job_runtime_error) + statemachine.add_transition_event(SState.CLEANUP, SState.READY, job_cleanup) def handle_job_packet(packet: pkt.DataPacket): """Handles all job packets sent by the Kamaji server""" diff --git a/Test-Rack-Software/TARS-Rack/util/handle_packets.py b/Test-Rack-Software/util/handle_packets.py similarity index 79% rename from Test-Rack-Software/TARS-Rack/util/handle_packets.py rename to Test-Rack-Software/util/handle_packets.py index 3af69ac..3d044c0 100644 --- a/Test-Rack-Software/TARS-Rack/util/handle_packets.py +++ b/Test-Rack-Software/util/handle_packets.py @@ -34,8 +34,17 @@ def handle_server_packets(Server: Datastreamer.DatastreamerServer): else: Server.packet_buffer.add(pkt.CL_INVALID(packet)) case pkt.DataPacketType.JOB: - # For JOB, we try to trigger a job. - jobs.handle_job_packet(packet) + # For JOB, we try to trigger a job if none exists + # If a job is running, send BUSY + if(Server.job_active): + Server.packet_buffer.add(pkt.CL_BUSY(Server.current_job_data)) + else: + jobs.handle_job_packet(packet) + case pkt.DataPacketType.TERMINATE: + Server.signal_abort = True + case pkt.DataPacketType.CYCLE: + Server.signal_abort = True + Server.signal_cycle = True diff --git a/Test-Rack-Software/TARS-Rack/util/packets.py b/Test-Rack-Software/util/packets.py similarity index 98% rename from Test-Rack-Software/TARS-Rack/util/packets.py rename to Test-Rack-Software/util/packets.py index 08f08b2..21dccca 100644 --- a/Test-Rack-Software/TARS-Rack/util/packets.py +++ b/Test-Rack-Software/util/packets.py @@ -54,6 +54,7 @@ class DataPacketType(int, Enum): # Misc packets RAW = 200 """[MISC] A completely raw datapacket""" + ERR = 201 class DataPacket: """ @@ -166,7 +167,10 @@ def write_buffer_to_serial(self, serial_port: serial.Serial) -> None: Writes the entire packet buffer to serial """ serialized_full: str = self.to_serialized_string() + if(len(self.packet_buffer) > 0): + serialized_full += "[[pkt_end]]" self.packet_buffer = [] + serial_port.write(serialized_full.encode()) @@ -194,6 +198,8 @@ def stream_to_packet_list(stream: str, safe_deserialize:bool=False) -> list[Data serialized_packets = stream.split("[[pkt_end]]") ds_packets: list[DataPacket] = [] for ser_pkt in serialized_packets: + if(ser_pkt == ""): + continue new_packet = DataPacket(DataPacketType.RAW, {}) if(safe_deserialize): new_packet.safe_deserialize(ser_pkt) @@ -371,7 +377,7 @@ def CL_DONE(job_data: JobData, hilsim_result:str) -> DataPacket: @job_data: The job data sent along with this packet (For ID purposes) @hilsim_result: Raw string of the HILSIM output""" packet_data = {'job_data': job_data.to_dict()} - return DataPacket(DataPacketType.READY, packet_data, hilsim_result) + return DataPacket(DataPacketType.DONE, packet_data, hilsim_result) def CL_INVALID(invalid_packet:DataPacket) -> DataPacket: """Constructs INVALID packet (RAW) @@ -443,6 +449,12 @@ def SV_JOB(job_data: JobData, flight_csv: str) -> DataPacket: packet_data = {'job_data': job_data.to_dict()} return DataPacket(DataPacketType.JOB, packet_data, flight_csv) +# Misc packets +def MISC_ERR(error: str) -> DataPacket: + """Constructs ERR packet""" + packet_data = {'error': error} + return DataPacket(DataPacketType.ERR, packet_data) + class PacketValidator: def is_server_packet(packet: DataPacket) -> bool: """Determines whether this packet is a server packet or not. Returns FALSE for misc packets.""" diff --git a/Test-Rack-Software/TARS-Rack/util/pio_commands.py b/Test-Rack-Software/util/pio_commands.py similarity index 91% rename from Test-Rack-Software/TARS-Rack/util/pio_commands.py rename to Test-Rack-Software/util/pio_commands.py index ebfec41..1a6d524 100644 --- a/Test-Rack-Software/TARS-Rack/util/pio_commands.py +++ b/Test-Rack-Software/util/pio_commands.py @@ -3,9 +3,16 @@ # # This script will run the remote-command.py wrapper commands for you. -import subprocess import os -import util.config as config +import sys + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + +import subprocess +import config as cfg + +config = cfg.use_meta #### Helper functions #### def run_script(arg_list): diff --git a/Test-Rack-Software/TARS-Rack/util/remote_command.py b/Test-Rack-Software/util/remote_command.py similarity index 84% rename from Test-Rack-Software/TARS-Rack/util/remote_command.py rename to Test-Rack-Software/util/remote_command.py index f6afc80..0b1378b 100644 --- a/Test-Rack-Software/TARS-Rack/util/remote_command.py +++ b/Test-Rack-Software/util/remote_command.py @@ -6,24 +6,35 @@ # The biggest caveat of this script is it depending on being run from a python shell. Luckily, git-commands.py # provides an abstraction layer for this script. +import os +import sys + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + import config from git import Repo -import sys -import os, shutil + +import util.avionics_meta + +config_meta: util.avionics_meta.PlatformMetaInterface = config.use_meta + + def clone_repo(): """ Clone the repository defined in config into the directory defined in config. (Usually ./remote) """ print("(git_commands) Cloning repository..") - Repo.clone_from(config.repository_url, config.remote_path) + print("ASDF", config_meta.repository_url) + Repo.clone_from(config_meta.repository_url, config_meta.remote_path) def reset_repo(): """ Reset the repository back to its "master" or "main" state by stashing current changes, switching to main, then pulling. """ - repo = Repo(config.remote_path) + repo = Repo(config_meta.remote_path) print("(git_commands) Stashing changes..") repo.git.checkout(".") print("(git_commands) Checking-out and pulling origin/master..") @@ -36,7 +47,7 @@ def pull_branch(branch): Switch to a specific branch and pull it @param branch The branch to pull from the remote defined in config. """ - repo = Repo(config.remote_path) + repo = Repo(config_meta.remote_path) print("(git_commands) Fetching repository data") repo.git.fetch() print("(git_commands) checking out and pulling origin/" + branch) diff --git a/Test-Rack-Software/TARS-Rack/util/serial_wrapper.py b/Test-Rack-Software/util/serial_wrapper.py similarity index 100% rename from Test-Rack-Software/TARS-Rack/util/serial_wrapper.py rename to Test-Rack-Software/util/serial_wrapper.py From d44ba2257bc4d17c6de895e67991e552bdb5799d Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Tue, 24 Oct 2023 18:07:59 -0500 Subject: [PATCH 13/24] removed extra print --- Test-Rack-Software/main.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Test-Rack-Software/main.py b/Test-Rack-Software/main.py index e942ca9..1597343 100644 --- a/Test-Rack-Software/main.py +++ b/Test-Rack-Software/main.py @@ -59,14 +59,6 @@ def check_server_connection(Server: Datastreamer.DatastreamerServer): if pkt.packet_type == packet.DataPacketType.ACKNOWLEDGE: Server.board_id = pkt.data['board_id'] Server.server_port = port - - #temporary, close all non-server ports: - - """for port in connection.connected_comports: - if(port.name != Server.server_port.name): - print("TEMP: closed " + port.name) - port.close()""" - return True return False From 5f73a58ad36e5c3831016fca8d71df6f6e0427a5 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 25 Oct 2023 15:15:42 -0500 Subject: [PATCH 14/24] Added more documentation --- Test-Rack-Software/README.md | 222 +++++++++++++++++++++++++ Test-Rack-Software/tars_rack/README.md | 50 ++---- Test-Rack-Software/util/README.md | 32 +++- 3 files changed, 263 insertions(+), 41 deletions(-) diff --git a/Test-Rack-Software/README.md b/Test-Rack-Software/README.md index e69de29..50221cc 100644 --- a/Test-Rack-Software/README.md +++ b/Test-Rack-Software/README.md @@ -0,0 +1,222 @@ +# Test Rack Software +**Brief:** Kamaji test racks are the backbone of the Kamaji ecosystem. They are the actual interfaces to our avionics stacks. They are virtually entirely self-sufficient (as in, they can run HILSIM without explicit direction of when to do what), however, they rely on Kamaji to give them **jobs**, abstractions on **what** we want the testing racks to do and with **what data**. + +In short, you can think of them as a black box, where you put in flight data and a git branch/git commit/other job configuration, and receive back a log of everything that the flight computer "thought of" during the flight. + +Below is the scheme of operations: +![[Pasted image 20231022215331.png]] + +The following documentation serves as a high level overview of the operations of the Test rack as a whole. + +#### Useful terminology: +**Locking** (In the context of an avionics stack) is the process which occurs when an avionics stack encounters a non-recoverable error and is thus unable to accept further input. The only fix to this is usually a power cycle (or re-flash) to the avionics system. + +**Peripherals** (In the context of an avionics stack) are hardware devices which do not include the altimeter, but can be otherwise tested, such as a camera board, breakout, or receiver. +# Datastreamer +The "brains" of the test rack, the Datastreamer "server" is what communicates with Kamaji and what orchestrates the setup and execution of jobs. + +*Disambiguation:* Datastreamer will be referred to in these docs and in code as a "server". This is technically not entirely accurate as its primary purpose isn't to serve its own content, it is to take messages ("packets") from Kamaji and transform them into an actual HILSIM run on the avionics bay by compiling the code and running the HILSIM job. + +#### Overview + +The **Datastreamer** consists mainly of two parts which work together to set up and execute jobs. The **Datastreamer Server** does not change between each test rack, and it handles all packet sending/recieveing, all server lifetime methods, and generally all functionality that does not require direct interaction with an avionics stack. + +The **avionics interface** is code that will be different for every testing rack. This code is ***required*** to have an `interface.py` file in the root (`Test-Rack-Software/mytestrack/interface.py`), and furthermore, `interface.py` *MUST* expose an `av_instance` variable and a `HilsimRun` class. The specifics of this behavior is explored further in the **Avionics Interfaces** section below. + +Because of the way that this code is structured, we follow a paradigm of **convention over configuration** when writing Datastreamer code, so a lot of functionality of the datastreamer server is dependent on finding certain resources in places where we expect them. For this reason, it is *extremely* important that ***all*** avionics-specific code is kept *ONLY* in its respective AV interface (i.e. `Test-Rack-Software/TARS-Rack`), while all Datastreamer code is kept in the `Test-Rack-Software` root directory or `Test-Rack-Software/util`. + +As a broad rule of thumb, `Datastreamer` does the *talking*, `Avionics Interfaces` do all the *doing*. +#### Lifetime / State +Because of how predictable we need the Datastreamer server to be, the design choice has been made to lock the state of the server to a known, finite amount of states in which it can possibly be. This makes Datastreamer state part of a **Finite state machine** (https://en.wikipedia.org/wiki/Finite-state_machine). The structure of the state machine and all possible transitions are shown in the diagram below: + +![[Pasted image 20231022232038.png]] + +The server by default begins in the **INIT** state, and each arrow between a state describes a transition between those states (internally, these transitions are called **pipes**). + +The state machine works by attempting to follow a transition pipe from it's current state every tick. It does so by executing every defined **transition function** for that transition, which are defined for a transition from state A to state B. Depending on the result of each transition function, the transition might or might not happen. If the transition happens, all the associated `success events` associated with that transition are also executed + +In other words, if the server wishes to transition from state A to state B, it must pass every check defined in the transition functions that link state A to state B. + +During each tick, the server also executes all of the **Always events** defined for the current server state. An **Always event** is linked to a specific state, or to the `ANY` state, and defines some code that should be run in that state every time that a server tick is triggered. + +##### Example: +Suppose you want to transition from the `AV_DETECT` to the `READY` state. You would define a pipe between the `AV_DETECT` and `READY` state, as well as a **Transition event** between those two states which attempts to detect an avionics stack, and returns true if it's successful in doing so (false otherwise). You may also want to define a **Success Event** that reports to Kamaji that this stack is ready. In this setup, whenever the server is in the `AV_DETECT` state, it will attempt to detect an avionics stack every tick until it is successful in doing so, after which it will switch to the `READY` state and send a response to the server. + +There are additionally ways to manually trigger transitions. By invoking `ServerStateController.try_transition(to_state)` you can "ask" the Datastreamer server to transition to a specific state, even if it's not linked by a pipe. It should be noted that it isn't advised to do this often, since transitioning from a state without cleaning up can lead to undefined side effects. If you find yourself wanting to transition from one state to another often, it is better to define a pipe and transition events for those two states. Furthermore, attempting to call `try_transition` from within a transition event callback will *always* fail if the callback is successful (as the callback executing successfully will switch the server back to the previous state, and this switching is undefined behavior) + +In the same vein, invoking `ServerStateController.force_transition(to_state)` will force the aforementioned transition to happen, without asking if transition events pass. This is generally used when errors are detected and you ***need*** to transition to an error state. + +#### Hardware +Physically, a test rack consists of some full-function computer (think: raspberry pi) connected to an avionics stack (Think: TARSmkIV or MIDAS). This whole system is then connected to the Kamaji server through the computer. + +An **avionics stack** primarily consists of **primary hardware** and 0 or more **peripherals**. As an example, the TARS mkIV stack consists of just the TARS boards themselves connected to a singular USB port on the test rack computer, while a MIDAS stack may consist of the MIDAS board as well as the SPARK breakout. + +The way that peripherals are implemented and detected is left as an exercise to the implementer, and can be either done by writing a separate AV interface or adapting the existing interface to support the new peripheral. + +As of now, all connections between systems are made with serial, this may be modified later on to allow for more board peripherals (since one of the serial ports is used for server communication). + +#### Packets +The datastreamer communicates to the Kamaji server through a system of packets. The purpose of each packet is well documented within `packets.py`, so it is recommended to check out what each packet does. In general, each packet has a **packet header** and an optional **raw_data** parameter. The packet header is encoded using JSON, while `raw_data` is usually encoded as a UTF-8 string. + +Packets are encoded to the serial buffer in the following manner as a string: `{packet_header}[[raw==>]]{raw_data}[[pkt_end]]`. This string can then be decoded back into the relevant packet, assuming that no data corruption happens. + +Because writing to serial directly can cause race conditions that corrupt packet data, you should instead write data between Kamaji and Datastreamer by using the `DataPacketBuffer` class, an instance of which is present in the singleton `Datastreamer` instance. + +In the server loop, data is written into the packet buffer, decoded, and saved as a list of `DataPacket`s. Then, all outgoing packets in the output buffer are serialized and sent. Only then does the server perform its relevant tick updates. This can most clearly be seen in `DatastreamerServer`'s `tick()` method: + +```py +if self.server_port != None: + # We clear out output buffer and also populate our input buffer from the server +    self.packet_buffer.write_buffer_to_serial(self.server_port) + self.packet_buffer.read_to_input_buffer(self.server_port) + +self.state.update_always() # Run all `always` events +self.state.update_transitions()  # Run all transition tests + +self.packet_buffer.clear_input_buffer() # Discard our input buffer so we can get a new one next loop +``` + +After performing all our server actions, we discard the input buffer so we can get the next packets sent by the server. + +##### Special packet flows: +When jobs are being run, there are a couple of events which the server expects to receive. Omitting these packets won't explicitly *break* anything, however, including them will allow for easier debugging and will increase readability of what the server is doing: + +While performing `HilsimRun.job_setup()`: +`JOB_UPDATE` with action `"ACCEPTED"` => This packet is already done for you in datastreamer, it signifies the datastreamer accepting the job +`JOB_UPDATE (SETUP)` with action `"COMPILE_READY"` => Signifies the datastreamer is ready to compile +`JOB_UPDATE (SETUP)` with action `"COMPILED"` => Signifies successful compilation +`JOB_UPDATE (RUNNING)` with action `"BEGIN"` => Signifies the beginning of the run + +#### Deference +Due to the fact that python does not natively support asynchronous code, we have implemented **server deference**, or the idea that the current control flow of the server is owned by the scope which is currently being run. Unfortunately, a side effect of this is the fact that functions which take a long time to execute and which are generally blocking tend to decrease how responsive the server can be. + +To avoid this lack of responsiveness, functions can *Defer* their execution to allow server state updates to come in and for packets to be read. If you are writing a transition event which you suspect will be blocking of the server's responsiveness, make sure to call `Server.defer()` periodically within your function so you can access the state without leaving the scope. + +## Avionics Interfaces +The **avionics interface** is actually quite similar to an interface in the traditional sense, as in there is a certain amount of functionality that is *expected* from each interface, but the implementation specifics are not important to the datastreamer server. + +To make a properly functioning avionics interface, there are a couple things that you ***must*** do. +For an interface that you are trying to create in `Test-Rack-Software/myinterface`, you *must* have an entrypoint file `Test-Rack-Software/myinterface/interface.py`. Furthermore, this file must expose three pieces of data: `av_interface`, an object of type (or sub-type) `AvionicsInterface`, a class called `HilsimRun`, and a reference to a **platform metadata file** (explained below) stored in `platform_meta`. Both of the abstract versions of `HilsimRun` (called `HilsimRunInterface`) and `AvionicsInterface` are defined in `/util/avionics_interface`. You are required to override every method in both classes, and you can extend these classes using python **Abstract Base Class** notation: + +```py +import util.avionics_interface as AVInterface +import util.datastreamer_server as Datastreamer + +... + +class MyAvionics(AVInterface.AvionicsInterface): + # implementation here... + + +class HilsimRun(AVInterface.HilsimRun): + # implementation here... + +av_interface = MyAvionics(Datastreamer.instance) # MUST have this instance! +``` + +The `AvionicsInterface` file contains documentation of what every overridden function must do. + +In general, the avionics interface you write will be a data container and set of helper methods used by your `HilsimRun` implementation to perform different actions such as setting up, executing, and cleaning up your run. + +In addition to the `interface.py` file, you must also write a `Test-Rack-Software/myinterface/platform_meta.py` file, which looks something like this: + +```py +import os + +board_type = "YOUR_BOARD_NAME" +repository_url = "BOARD_GITHUB_REPOSITORY_URL" +platformio_subdirectory = "PLATFORMIO_DIRECTORY" # Relative to repo root + +# These usually stay the same, but can be modified if needed: +remote_path = os.path.dirname(__file__) # Where to clone ./remote +platformio_path = os.path.join(remote_path, platformio_subdirectory) +``` + +Once again, this is an example of convention over configuration. The datastreamer (and utility functions) *will* look for this (or, more accurately, for its reference in `platform.py`) to figure out where to put things, so it is imperative that this file is present. + +Finally, you must edit `Test-Rack-Software/config.py` to expose your avionics interface and metadata. +# Avionics + +In brief, this is the second part of each test rack. Because the avionics stack is unable to interpret what we want to test directly (we can't just ask the stack "can you compile branch av-30000-av-stuff"), we require Datastreamer to talk to it. Unfortunately, since the architecture of each avionics stack is different we are required to write a seperate interface between the avionics stack and datastreamer. + +It should be noted that in the context of "avionics", this system refers to the **primary hardware** *and* the peripherals taken together. + +So far the avionics stacks supported on Kamaji/Hilsim server are +- **TARSmkIV** + + +# Getting started with development: +This is a quick-start guide to making your own avionics interface. + +First, create a new subfolder under the directory `Test-Rack-Software`. For this tutorial, we will be using the name `my_test_rack`, but of course this can be called anything (but `util`). + +Next, under `Test-Rack-Software/my_test_rack`, create the two **required** files `interface.py` and `platform_meta.py` + +You may then copy over another test rack's code and tweak it, or work off of this skeleton: + +**Test-Rack-Software/my_test_rack/platform_meta.py** +```py +import util.avionics_meta as AVMeta + +class PlatformMeta(AVMeta.PlatformMetaInterface): +    board_type = "AV_TYPE" # Avionics type +    repository_url = "GIT_REPO_URL" # Software repository +    platformio_subdirectory = "./PIO_SUBDIRECTORY" # Platformio subdirectory + +    def __init__(self, file: str) -> None: +        super().__init__(file) + +meta = PlatformMeta(__file__) +``` + +**Test-Rack-Software/my_test_rack/interface.py** +```py +import util.avionics_interface as AVInterface +import util.datastreamer_server as Datastreamer + +class MyAvionics(AVInterface.AvionicsInterface): +    TARS_port: serial.Serial = None + +    def handle_init(self) -> None: +        return super().handle_init() + +    def detect(self) -> bool: + return super().detect() + +    def first_setup(self) -> None: + return super().first_setup() + +    def code_reset(self) -> None: + return super().code_reset() + +    def power_cycle(self) -> bool: + return super().power_cycle() + +    def code_pull(self, git_target) -> None: + return super().code_pull() + +    def code_flash(self) -> None: + return super().code_flash() + +class HilsimRun(AVInterface.HilsimRunInterface): +    av_interface: MyAvionics # Specify your AV interface + +    def get_current_log(self) -> str: + return super().get_current_log() + + + +    def job_setup(self): + return super().job_setup() + +    def __init__(self, datastreamer: Datastreamer.DatastreamerServer, av_interface: TARSAvionics, raw_csv: str, job: pkt.JobData) -> None: +        super().__init__(datastreamer, av_interface, raw_csv, job) +        +    def post_setup(self) -> None: + return super().post_setup() + +    def step(self, dt: float): + return super().step() + +av_instance = MyAvionics(Datastreamer.instance) # Important! +``` \ No newline at end of file diff --git a/Test-Rack-Software/tars_rack/README.md b/Test-Rack-Software/tars_rack/README.md index 9093917..4cf2345 100644 --- a/Test-Rack-Software/tars_rack/README.md +++ b/Test-Rack-Software/tars_rack/README.md @@ -1,48 +1,18 @@ ## HILSIM Data Streamer - TARS -This is the directory storing the code for the TARS data-streaming module to be used with HILSIM server. Feel free to clone this repository and edit it to suit your specific use-cases. -##### DELETEME -I've written a couple of wrapper libraries around GitPython and Platformio which should make development much simpler. They are located in `git_commands` and `pio_commands` respectively, I suggest taking a look into each one, but they're both quite simple: +This is the directory storing the code for the TARS data-streaming module to be used with the Kamaji service. -**git_commands** exposes 4 functions, which each call the `remote_commands` script with different parameters. `remote_commands` is the *actual* wrapper around GitPython (if you're interested in why I chose to do it this way, feel free to ask!) and doesn't need to be edited directly unless you wish to add functionality. The other two libraries will be (to some extent) dependent on the actual architecture we're uploading stuff. +> [!IMPORTANT] +> This avionics stack type **DOES NOT** support power cycling -**What is implemented for you:** -`util/git_commands.py` -- This is a library that performs all the git actions you will need for this application
-`util/pio_commands.py` -- Library to handle all the pio commands you will need for this application
-`util/packets.py` -- Constructors for all communication packets we'll be sending.
-`util/serial_wrapper.py` -- **Not fully implemented!** IDENT functionality and packet decoding is implemented for you, but you will need to handle the other packets (explained below).
-`av_platform/run_setup.py` -- **Not fully implemented!** I took the liberty of doing the easy ones, you need to implement the platform-specific `flash` command and the platform specific `run_hilsim` command.
+> [!IMPORTANT] +> This avionics stack is susceptible to **LOCKING** (Non-recoverable errors) -**What needs to be implemented:** -The rest of `util/serial_wrapper.py` -- Implement communication with the server (fun part)!
-`main.py` -- The actual server itself
-The rest of `av_platform/run_setup.py` -- Implement platform-specific code pushing and hilsim runs.
+#### Differences in the Avionics Interface +`detect_avionics` +Usually you would want to detect avionics by reading their output and confirming that the information they're giving you is consistent with that av stack. This interface doesn't need to be as sophisticated (as **TARS** does not have any peripherals). The interface will treat the first non-server port taken up as the avionics target port. -Here are your technical requirements for server communication: -- Whenever an `IDENT?` packet is recieved, IF this board was assigned an id before, send an ID-CONF packet with the board type and board id stored. OTHERWISE, send an IDENT packet. -- Whenever an `ACK` packet is recieved, store the board ID assigned and which port sent it (This will be the server port). Then, send a `READY` packet. -- Whenever a `REASSIGN` packet is recieved, change to board_id but do not terminate any jobs. -- Whenever a `TERMINATE` packet is recieved, immediately terminate all currently running jobs, then, send a `READY` packet. -- Whenever a `CYCLE` packet is recieved, terminate all currently running jobs. If the current platform supports power cycling, then power cycle the test stand, then send a `READY` packet. If the current platform cannot power cycle, immediately send a `READY` packet. -- `stream_data` runs in a while loop, so to implement the above two bullet points you will need to figure out how to communicate to the server while running a hilsim job. (I don't know exactly how to do that so go ham) -- Whenever a `JOB` packet is recieved, IF a job is currently running, send a `BUSY` packet back with the currently running job data. Otherwise, run the job and send `JOB-UPD` packets with the status of the job while it's running (The first status should always be `"Accepted"`) -- Whenever a `PING` packet is recieved, send a `PONG` packet immediately. +> [!WARNING] +> When using this avionics interface, do **not** plug in other peripherals into the datastreamer computer, as this may cause a mismatch between the platformio target and the datastreamer av target. -Most of the places where code needs to be implemented is marked with a `TODO`. Good luck! -##### END DELETEME - -## Wrapper script reference -The `util/` directory contains multiple wrapper scripts written for development convenience. They expose functions to interface with Git and with Platformio. Their exposed functions are below: - -**git_commands**.py: -- `run_script(str[] args)` --> Runs `remote_command.py` with the specified arguments (Used in the definition of all the other functions) -- `remote_clone()` --> Clones the remote into the directory specified in `config.py` -- `remote_reset()` --> Discards all changes and checks out/pulls `origin/main`. Runs `remote_clone` first if the remote does not exist. -- `remote_pull_branch(str branch)` --> Checks out and pulls the branch `branch` from the remote. Runs `remote_clone` first if the remote does not exist. - -**pio_commands**.py: -- `run_script(str[] args)` --> Runs `platformio` with the specified arguments. -- `pio_build(str build_target)` --> Runs `platformio run`. If `build_target` is specified, then it adds the `--environment` flag with `build_target`. -- `pio_upload(str build_target)` --> Runs `platformio run --target upload`. If `build_target` is specified, then it adds the `--environment` flag with `build_target`. (Flashes code once the build completes) -- `pio_clean(str build_target)` --> Runs `platformio run --target clean`. If `build_target` is specified, then it adds the `--environment` flag with `build_target`. (Does a clean build) diff --git a/Test-Rack-Software/util/README.md b/Test-Rack-Software/util/README.md index 96e14d5..90e002b 100644 --- a/Test-Rack-Software/util/README.md +++ b/Test-Rack-Software/util/README.md @@ -1,3 +1,33 @@ # Utilities directory -This directory contains all utility functions and libraries written to make flashing and running code easier. In an ideal world, the contents of this directory are untouched except to add new utilities. \ No newline at end of file +This directory contains all utility functions and libraries written to make flashing and running code easier. In an ideal world, the contents of this directory are untouched except to add new utilities. + +`avionics_interface.py` +Provides interfaces for the `AvionicsInterface` and `HilsimRun` classes. + +`avionics_meta.py` +Provides an interface for the required `platform_meta.py` file. + +`datastreamer_server.py` +Provides a class for state and data management for datastreamer. + +`git_commands.py` +A wrapper for `remote_command.py` (itself a wrapper for GitPy). + +`handle_jobs.py` +A helper file for handling all job-related functions (Hooks into datastreamer functionality) + +`handle_packets.py` +A helper file for handling all server packets (Hooks into datastreamer functionality) + +`packets.py` +Establishes packet communication protocol for datastreamer. + +`pio_commands.py` +Wrapper for **platformio** + +`remote_command.py` +Memory safe wrapper for GitPy (which leaks memory) + +`serial_wrapper` +Provides some initialization functions for serial \ No newline at end of file From 32577b847ecc5d7c3c743f2fafdcb0d856ca10f6 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 25 Oct 2023 15:23:09 -0500 Subject: [PATCH 15/24] fixed docs --- Test-Rack-Software/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Test-Rack-Software/README.md b/Test-Rack-Software/README.md index 50221cc..5b003b8 100644 --- a/Test-Rack-Software/README.md +++ b/Test-Rack-Software/README.md @@ -4,7 +4,7 @@ In short, you can think of them as a black box, where you put in flight data and a git branch/git commit/other job configuration, and receive back a log of everything that the flight computer "thought of" during the flight. Below is the scheme of operations: -![[Pasted image 20231022215331.png]] +![Kamaji-Datastreamer scheme of operations](https://i.ibb.co/sV0qTsg/Pasted-image-20231022215331.png) The following documentation serves as a high level overview of the operations of the Test rack as a whole. @@ -29,7 +29,7 @@ As a broad rule of thumb, `Datastreamer` does the *talking*, `Avionics Interface #### Lifetime / State Because of how predictable we need the Datastreamer server to be, the design choice has been made to lock the state of the server to a known, finite amount of states in which it can possibly be. This makes Datastreamer state part of a **Finite state machine** (https://en.wikipedia.org/wiki/Finite-state_machine). The structure of the state machine and all possible transitions are shown in the diagram below: -![[Pasted image 20231022232038.png]] +![Kamaji-FSM](https://i.ibb.co/kBwD8BX/Pasted-image-20231022232038.png) The server by default begins in the **INIT** state, and each arrow between a state describes a transition between those states (internally, these transitions are called **pipes**). @@ -204,8 +204,6 @@ class HilsimRun(AVInterface.HilsimRunInterface):     def get_current_log(self) -> str: return super().get_current_log() - -     def job_setup(self): return super().job_setup() @@ -219,4 +217,6 @@ class HilsimRun(AVInterface.HilsimRunInterface): return super().step() av_instance = MyAvionics(Datastreamer.instance) # Important! -``` \ No newline at end of file +``` + +This should work out of the box, of course, until you implement the required methods, the base class will throw exceptions. \ No newline at end of file From 4dd72a6a7d376d5b0e4bb61821fce4d62338147d Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 25 Oct 2023 15:24:40 -0500 Subject: [PATCH 16/24] removed tars reference from shared ds docs --- Test-Rack-Software/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/Test-Rack-Software/README.md b/Test-Rack-Software/README.md index 5b003b8..d5ac3df 100644 --- a/Test-Rack-Software/README.md +++ b/Test-Rack-Software/README.md @@ -175,8 +175,6 @@ import util.avionics_interface as AVInterface import util.datastreamer_server as Datastreamer class MyAvionics(AVInterface.AvionicsInterface): -    TARS_port: serial.Serial = None -     def handle_init(self) -> None:         return super().handle_init() From a3b208a12ec8994bf09ad846084c62dcd5c29ba2 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 25 Oct 2023 16:20:42 -0500 Subject: [PATCH 17/24] removed non-related files --- Central-Server/API/services/dummy_port.py | 9 -- .../API/services/hilsim_constructor.py | 19 ---- .../API/services/job_constructor.py | 97 ------------------- Central-Server/API/services/job_creator.py | 0 Central-Server/API/services/job_queue.py | 0 .../API/services/queue_reconstructor.py | 0 .../API/services/serial_responder.py | 58 ----------- Test-Rack-Software/config.py | 11 +-- 8 files changed, 1 insertion(+), 193 deletions(-) delete mode 100644 Central-Server/API/services/dummy_port.py delete mode 100644 Central-Server/API/services/hilsim_constructor.py delete mode 100644 Central-Server/API/services/job_constructor.py delete mode 100644 Central-Server/API/services/job_creator.py delete mode 100644 Central-Server/API/services/job_queue.py delete mode 100644 Central-Server/API/services/queue_reconstructor.py delete mode 100644 Central-Server/API/services/serial_responder.py diff --git a/Central-Server/API/services/dummy_port.py b/Central-Server/API/services/dummy_port.py deleted file mode 100644 index 598bcc3..0000000 --- a/Central-Server/API/services/dummy_port.py +++ /dev/null @@ -1,9 +0,0 @@ -import serial -import json - -port = serial.Serial("COM9", timeout=5, write_timeout=0) - -print(json.loads('{"packet_type": 4, "packet_data": {}, "use_raw": true}')) - -while True: - pass \ No newline at end of file diff --git a/Central-Server/API/services/hilsim_constructor.py b/Central-Server/API/services/hilsim_constructor.py deleted file mode 100644 index ba1e352..0000000 --- a/Central-Server/API/services/hilsim_constructor.py +++ /dev/null @@ -1,19 +0,0 @@ -# HILSIM Job Constructor -# This service will be responsible for taking in job IDs and sending raw job data over Serial to the Data Streamer app -# running on the Raspberry Pi devices that we're running. -# Michael Karpov (2027) - -import serial_tester -import sys -import serial -import os - -sys.path.insert(0, os.path.abspath( - os.path.join(os.path.dirname(__file__), '..'))) - -import util.packets as pkt - - - -if __name__ == "__main__": - pass \ No newline at end of file diff --git a/Central-Server/API/services/job_constructor.py b/Central-Server/API/services/job_constructor.py deleted file mode 100644 index f79e389..0000000 --- a/Central-Server/API/services/job_constructor.py +++ /dev/null @@ -1,97 +0,0 @@ -# HILSIM Job Constructor -# This service will be responsible for taking in job IDs and sending raw job data over Serial to the Data Streamer app -# running on the Raspberry Pi devices that we're running. -# Michael Karpov (2027) - -import serial_tester -import sys -import serial -import os - -sys.path.insert(0, os.path.abspath( - os.path.join(os.path.dirname(__file__), '..'))) - -import util.packets as pkt - - - - -comport = "COM8" - -if __name__ == "__main__": - print("Detected script running as __main__, beginning test of Data Streamer functionality") - serial_tester.SECTION("Test setup") - port = serial_tester.TRY_OPEN_PORT(comport) - - # Setup - serial_tester.RESET_TEST(port) - - # > PING - serial_tester.SECTION("PING - Signal testing") - serial_tester.TRY_WRITE(port, pkt.construct_ping().encode(), "Writing PING packet") - serial_tester.TEST("Responds to PING packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Complies to PONG packet format", serial_tester.VALID_PACKET(port, "PONG", valid, type, data)) - - # > IDENT? - serial_tester.SECTION("IDENT? - Identity confirmation") - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet") - serial_tester.TEST("Responds to IDENT? packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("Complies to ID-CONF packet format", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) - - # > ACK 1 - ack_test_boardid = 4 - serial_tester.SECTION("[1/2] ACK - Valid Acknowledge packet") - serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing valid ACK packet") - serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after ACK packet") - serial_tester.TEST("Responds to IDENT? packet after ACK packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("IDENT? packet after ACK packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) - - cond = False - res = "ID-CONF does not return correct board ID after ACK (Expected " + str(ack_test_boardid) + ", but got " + str(data['board_id']) + ")" - if(data['board_id'] == ack_test_boardid): - cond = True - res = "ID-CONF correctly returned board ID " + str(ack_test_boardid) - serial_tester.TEST("Connected board returns properly set ID", (cond, res)) - - # > ACK 2 (invalid) - ack_test_boardid = 0 - serial_tester.SECTION("[2/2] ACK - Acknowledge packet after ACK") - serial_tester.TRY_WRITE(port, pkt.construct_acknowledge(ack_test_boardid).encode(), "Writing invalid ACK packet") - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("INVALID packet after second ACK packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) - - # > REASSIGN 1 - reassign_test_boardid = 2 - serial_tester.SECTION("REASSIGN - [Valid] Assign new board ID to rack") - serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing REASSIGN packet") - serial_tester.TEST("No response from application [Success]", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) - serial_tester.TRY_WRITE(port, pkt.construct_ident_probe().encode(), "Writing IDENT? packet after REASSIGN packet") - serial_tester.TEST("Responds to IDENT? packet after REASSIGN packet", serial_tester.AWAIT_ANY_RESPONSE(port)) - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("IDENT? packet after REASSIGN packet conforms to ID-CONF", serial_tester.VALID_PACKET(port, "ID-CONF", valid, type, data)) - - cond = False - res = "ID-CONF does not return correct board ID after REASSIGN (Expected " + str(reassign_test_boardid) + ", but got " + str(data['board_id']) + ")" - if(data['board_id'] == reassign_test_boardid): - cond = True - res = "ID-CONF correctly returned board ID " + str(reassign_test_boardid) - serial_tester.TEST("Connected board returns properly set ID", (cond, res)) - - # > REASSIGN 2 - reassign_test_boardid = 8 - serial_tester.RESET_TEST(port) - serial_tester.SECTION("REASSIGN - [Invalid] Assign new board ID to fresh rack") - serial_tester.TRY_WRITE(port, pkt.construct_reassign(reassign_test_boardid).encode(), "Writing invalid REASSIGN packet") - valid, type, data = serial_tester.GET_PACKET(port) - serial_tester.TEST("INVALID packet after non-initialized REASSIGN packet", serial_tester.VALID_PACKET(port, "INVALID", valid, type, data)) - - # CLEANUP - serial_tester.SECTION("Cleanup") - serial_tester.TEST("Ensure empty serial bus", serial_tester.ENSURE_NO_RESPONSE(port, 1.0)) - serial_tester.RESET_TEST(port) - serial_tester.DONE() - diff --git a/Central-Server/API/services/job_creator.py b/Central-Server/API/services/job_creator.py deleted file mode 100644 index e69de29..0000000 diff --git a/Central-Server/API/services/job_queue.py b/Central-Server/API/services/job_queue.py deleted file mode 100644 index e69de29..0000000 diff --git a/Central-Server/API/services/queue_reconstructor.py b/Central-Server/API/services/queue_reconstructor.py deleted file mode 100644 index e69de29..0000000 diff --git a/Central-Server/API/services/serial_responder.py b/Central-Server/API/services/serial_responder.py deleted file mode 100644 index 7ce844e..0000000 --- a/Central-Server/API/services/serial_responder.py +++ /dev/null @@ -1,58 +0,0 @@ -import serial -import time -import json -import sys -import os - -stored_board_id = -1 - -sys.path.insert(0, os.path.abspath( - os.path.join(os.path.dirname(__file__), '..'))) - -import util.packets as pkt -import util.client_packets as cl_pkt - -ser = serial.Serial("COM9") -print("connected") - -def tobinary(dict): - return (json.dumps(dict) + "\n").encode() - -if ser.in_waiting: - data = ser.read_all() - print("Cleared " + str(len(data)) + " bytes from memory") - -while True: - if ser.in_waiting: - data = ser.read_all() - string = data.decode("utf8") - print("Got " + string) - - if string: - if string == "!test_reset": - stored_board_id = -1 - print("RESET TEST ENV \n\n") - continue - obj = json.loads(string) - valid, type, data = cl_pkt.decode_packet(string) - - match type: - case "PING": - print("Sending: ", cl_pkt.construct_pong()) - ser.write(tobinary(cl_pkt.construct_pong())) - case "IDENT?": - print("Sending: ", cl_pkt.construct_id_confirm("TARS", stored_board_id)) - ser.write(tobinary(cl_pkt.construct_id_confirm("TARS", stored_board_id))) - case "ACK": - print(stored_board_id) - if(stored_board_id == -1): - stored_board_id = int(data['board_id']) - else: - print("Sending: ", cl_pkt.construct_invalid(string)) - ser.write(tobinary(cl_pkt.construct_invalid(string))) - case "REASSIGN": - if(stored_board_id != -1): - stored_board_id = int(data['board_id']) - else: - print("Sending: ", cl_pkt.construct_invalid(string)) - ser.write(tobinary(cl_pkt.construct_invalid(string))) \ No newline at end of file diff --git a/Test-Rack-Software/config.py b/Test-Rack-Software/config.py index 36bb116..d0abd9d 100644 --- a/Test-Rack-Software/config.py +++ b/Test-Rack-Software/config.py @@ -22,15 +22,6 @@ # =============================================================== - - - - - - - -# ========================= STOP! =========================== -# This is the end of the basic configuration, proceed only if you know what you are doing - +# Post-config setup # nothing here lol print("(config) Configuration loaded") From 8ff19c6c84d5f7a56528abd9c0d4092a7878c1db Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Sun, 5 Nov 2023 11:53:22 -0600 Subject: [PATCH 18/24] Initial communication channel code -- main runs, need to perform tests with hardware to check --- .../API/services/datastreamer_test.py | 4 +-- Central-Server/API/services/serial_tester.py | 2 +- Test-Rack-Software/main.py | 8 ++--- .../tars_rack/av_platform/stream_data.py | 2 +- Test-Rack-Software/tars_rack/interface.py | 2 +- Test-Rack-Software/util/avionics_interface.py | 2 +- .../communication/communication_interface.py | 35 +++++++++++++++++++ .../util/communication/ds_serial.py | 34 ++++++++++++++++++ .../util/{ => communication}/packets.py | 25 +++++-------- .../util/datastreamer_server.py | 17 ++++----- Test-Rack-Software/util/handle_jobs.py | 2 +- Test-Rack-Software/util/handle_packets.py | 2 +- Test-Rack-Software/util/serial_wrapper.py | 23 ++++++------ 13 files changed, 111 insertions(+), 47 deletions(-) create mode 100644 Test-Rack-Software/util/communication/communication_interface.py create mode 100644 Test-Rack-Software/util/communication/ds_serial.py rename Test-Rack-Software/util/{ => communication}/packets.py (96%) diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index e9c71bb..8986241 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -8,12 +8,12 @@ import os import time import json -import util.packets as pkt +import util.communication.packets as pkt sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) -import util.packets as pkt +import util.communication.packets as pkt comport = "COM9" diff --git a/Central-Server/API/services/serial_tester.py b/Central-Server/API/services/serial_tester.py index e8cd5a4..2187006 100644 --- a/Central-Server/API/services/serial_tester.py +++ b/Central-Server/API/services/serial_tester.py @@ -9,7 +9,7 @@ sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) -import util.packets as pkt +import util.communication.packets as pkt success = 0 fails = 0 diff --git a/Test-Rack-Software/main.py b/Test-Rack-Software/main.py index 1597343..54c9d71 100644 --- a/Test-Rack-Software/main.py +++ b/Test-Rack-Software/main.py @@ -11,7 +11,7 @@ import config as test_board_config import util.serial_wrapper as connection -import util.packets as packet +import util.communication.packets as packet import time import util.avionics_meta as AVMeta import util.handle_packets as handle_packets @@ -54,11 +54,11 @@ def send_wide_ident(Server: Datastreamer.DatastreamerServer): def check_server_connection(Server: Datastreamer.DatastreamerServer): for port in connection.connected_comports: - in_packets = packet.DataPacketBuffer.serial_to_packet_list(port, True) + in_packets = packet.DataPacketBuffer.channel_to_packet_list(port, True) for pkt in in_packets: if pkt.packet_type == packet.DataPacketType.ACKNOWLEDGE: Server.board_id = pkt.data['board_id'] - Server.server_port = port + Server.server_comm_channel = port return True return False @@ -155,7 +155,7 @@ def main(): Server.tick() # Actions that always happen: - if(Server.server_port != None and should_heartbeat(Server)): + if(Server.server_comm_channel != None and should_heartbeat(Server)): print("(heartbeat) Sent update at u+", time.time()) Server.packet_buffer.add(packet.CL_HEARTBEAT(packet.HeartbeatServerStatus(Server.state.server_state, Server.server_start_time, False, False), packet.HeartbeatAvionicsStatus(False, ""))) diff --git a/Test-Rack-Software/tars_rack/av_platform/stream_data.py b/Test-Rack-Software/tars_rack/av_platform/stream_data.py index 91e9f2a..ee3aece 100644 --- a/Test-Rack-Software/tars_rack/av_platform/stream_data.py +++ b/Test-Rack-Software/tars_rack/av_platform/stream_data.py @@ -6,7 +6,7 @@ import pandas import traceback import io -import util.packets as packet +import util.communication.packets as packet def raw_csv_to_dataframe(raw_csv): diff --git a/Test-Rack-Software/tars_rack/interface.py b/Test-Rack-Software/tars_rack/interface.py index 64f1492..d237301 100644 --- a/Test-Rack-Software/tars_rack/interface.py +++ b/Test-Rack-Software/tars_rack/interface.py @@ -14,7 +14,7 @@ import io import time import serial -import util.packets as pkt +import util.communication.packets as pkt import traceback import util.avionics_interface as AVInterface import util.datastreamer_server as Datastreamer diff --git a/Test-Rack-Software/util/avionics_interface.py b/Test-Rack-Software/util/avionics_interface.py index fd6c1d0..e951c15 100644 --- a/Test-Rack-Software/util/avionics_interface.py +++ b/Test-Rack-Software/util/avionics_interface.py @@ -1,5 +1,5 @@ import util.datastreamer_server as Datastreamer -import util.packets as pkt +import util.communication.packets as pkt from abc import ABC, abstractmethod # ABC = Abstract Base Classes import os diff --git a/Test-Rack-Software/util/communication/communication_interface.py b/Test-Rack-Software/util/communication/communication_interface.py new file mode 100644 index 0000000..f52e99a --- /dev/null +++ b/Test-Rack-Software/util/communication/communication_interface.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod # ABC = Abstract Base Classes + +class CommunicationChannel(ABC): + is_open: bool = False + """An extendable class representing a single communication channel. Exposes methods for + sending string-type data, reading string-type data, and other helper methods.""" + @abstractmethod + def write(self, data: str) -> None: + """Writes data to the channel""" + raise NotImplementedError("This communication channel's 'write' method has not been defined") + + @abstractmethod + def read(self) -> str: + """Reads all of the data from the channel""" + raise NotImplementedError("This communication channel's 'read' method has not been defined") + + @abstractmethod + def waiting_in(self) -> bool: + """Returns whether or not this communication channel has data in the input buffer""" + raise NotImplementedError("This communication channel's 'waiting_in' method has not been defined") + + @abstractmethod + def waiting_out(self) -> bool: + """Returns whether or not this communication channel has data in the output buffer""" + raise NotImplementedError("This communication channel's 'waiting_out' method has not been defined") + + @abstractmethod + def open(self) -> None: + """Opens the channel if it is closed""" + raise NotImplementedError("This communication channel's 'open' method has not been defined") + + @abstractmethod + def close(self) -> None: + """Closes the channel if it is open""" + raise NotImplementedError("This communication channel's 'close' method has not been defined") \ No newline at end of file diff --git a/Test-Rack-Software/util/communication/ds_serial.py b/Test-Rack-Software/util/communication/ds_serial.py new file mode 100644 index 0000000..7c230c2 --- /dev/null +++ b/Test-Rack-Software/util/communication/ds_serial.py @@ -0,0 +1,34 @@ +import util.communication.communication_interface as communication_interface +import serial + +class SerialChannel(communication_interface.CommunicationChannel): + serial_port: serial.Serial = None + def __init__(self, serial_port: serial.Serial) -> None: + self.serial_port = serial_port + self.is_open = serial_port.is_open + + def open(self) -> None: + self.serial_port.open() + + def close(self) -> None: + self.serial_port.close() + + def waiting_in(self) -> bool: + return self.serial_port.in_waiting > 0 + + def waiting_out(self) -> bool: + return self.serial_port.out_waiting > 0 + + def read(self) -> str: + if(self.serial_port.in_waiting): + instr = "" + while(self.serial_port.in_waiting): + data = self.serial_port.read_all() + string = data.decode("utf8") + if string: + instr += string + return instr + return "" + + def write(self, data: str) -> None: + self.serial_port.write(data.encode()) diff --git a/Test-Rack-Software/util/packets.py b/Test-Rack-Software/util/communication/packets.py similarity index 96% rename from Test-Rack-Software/util/packets.py rename to Test-Rack-Software/util/communication/packets.py index 21dccca..efbbab6 100644 --- a/Test-Rack-Software/util/packets.py +++ b/Test-Rack-Software/util/communication/packets.py @@ -4,6 +4,7 @@ import json from enum import Enum import serial +import util.communication.communication_interface as communication_interface class PacketDeserializeException(Exception): "Raised a packet fails to deserialize" @@ -162,7 +163,7 @@ def add(self, packet: DataPacket) -> None: """Adds a packet to the packet buffer""" self.packet_buffer.append(packet) - def write_buffer_to_serial(self, serial_port: serial.Serial) -> None: + def write_buffer_to_channel(self, channel: communication_interface.CommunicationChannel) -> None: """ Writes the entire packet buffer to serial """ @@ -170,16 +171,14 @@ def write_buffer_to_serial(self, serial_port: serial.Serial) -> None: if(len(self.packet_buffer) > 0): serialized_full += "[[pkt_end]]" self.packet_buffer = [] - - serial_port.write(serialized_full.encode()) - + channel.write(serialized_full) - def write_packet(packet: DataPacket, serial_port: serial.Serial) -> None: + def write_packet(packet: DataPacket, channel: communication_interface.CommunicationChannel) -> None: """ Writes a single packet to serial WARNING: using `write_packet` twice in a row will cause a deserialization error to be thrown due to a lack of a delimeter. """ - serial_port.write(packet.serialize().encode()) + channel.write(packet.serialize()) def to_serialized_string(self) -> None: """ @@ -217,18 +216,12 @@ def read_to_input_buffer(self, port: serial.Serial) -> None: for in_packet in in_buffer: self.input_buffer.append(in_packet) - def serial_to_packet_list(port: serial.Serial, safe_deserialize:bool=False) -> list[DataPacket]: + def channel_to_packet_list(channel: communication_interface.CommunicationChannel, safe_deserialize:bool=False) -> list[DataPacket]: """ - Converts all packets in a serial port's input buffer into a list of packets. + Converts all packets in a channel's input buffer into a list of packets. """ - if(port.in_waiting): - instr = "" - while(port.in_waiting): - data = port.read_all() - string = data.decode("utf8") - if string: - instr += string - return DataPacketBuffer.stream_to_packet_list(instr, safe_deserialize) + if(channel.waiting_in()): + return DataPacketBuffer.stream_to_packet_list(channel.read(), safe_deserialize) return [] def clear_input_buffer(self): diff --git a/Test-Rack-Software/util/datastreamer_server.py b/Test-Rack-Software/util/datastreamer_server.py index 6d786a4..8631b6b 100644 --- a/Test-Rack-Software/util/datastreamer_server.py +++ b/Test-Rack-Software/util/datastreamer_server.py @@ -5,7 +5,8 @@ sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) -import util.packets as packet +import util.communication.packets as packet +import util.communication.communication_interface as communication_interface import time import serial from enum import Enum @@ -199,7 +200,7 @@ class DatastreamerServer: state: ServerStateController = ServerStateController() """Datastreamer server's state machine reference""" board_type: str = "" - server_port: serial.Serial = None + server_comm_channel: communication_interface.CommunicationChannel = None """Serial port reference to the serial port that connects to the Kamaji server.""" board_id = -1 """Board ID assigned by the Kamaji server""" @@ -234,12 +235,12 @@ def tick(self): """Generic single action on the server. Will perform all server actions by calling transition events and always events. This function will generally be run within an infinite while loop.""" - if self.server_port != None: + if self.server_comm_channel != None: # We clear out output buffer and also populate our input buffer from the server if(len(self.packet_buffer.packet_buffer) > 0): print(self.packet_buffer.to_serialized_string()) - self.packet_buffer.write_buffer_to_serial(self.server_port) - self.packet_buffer.read_to_input_buffer(self.server_port) + self.packet_buffer.write_buffer_to_channel(self.server_comm_channel) + self.packet_buffer.read_to_input_buffer(self.server_comm_channel) self.state.update_always() # Run all `always` events self.state.update_transitions() # Run all transition tests @@ -250,10 +251,10 @@ def defer(self): This function allows a single execution of the server tick from any scope that has access to the server. This server tick does not do any transitions, since that control is held by the scope you call this function from. """ - if self.server_port != None: + if self.server_comm_channel != None: # We clear out output buffer and also populate our input buffer from the server - self.packet_buffer.write_buffer_to_serial(self.server_port) - self.packet_buffer.read_to_input_buffer(self.server_port) + self.packet_buffer.write_buffer_to_channel(self.server_comm_channel) + self.packet_buffer.read_to_input_buffer(self.server_comm_channel) self.state.update_always() # Run all `always` events self.packet_buffer.clear_input_buffer() diff --git a/Test-Rack-Software/util/handle_jobs.py b/Test-Rack-Software/util/handle_jobs.py index d89c6dc..602ec01 100644 --- a/Test-Rack-Software/util/handle_jobs.py +++ b/Test-Rack-Software/util/handle_jobs.py @@ -1,5 +1,5 @@ import util.datastreamer_server as Datastreamer -import util.packets as pkt +import util.communication.packets as pkt import config as test_board_config import util.avionics_interface as AVInterface import time diff --git a/Test-Rack-Software/util/handle_packets.py b/Test-Rack-Software/util/handle_packets.py index 3d044c0..dea979a 100644 --- a/Test-Rack-Software/util/handle_packets.py +++ b/Test-Rack-Software/util/handle_packets.py @@ -1,7 +1,7 @@ # This script handles all incoming packets as an ALWAYS action on the ANY state. import util.datastreamer_server as Datastreamer -import util.packets as pkt +import util.communication.packets as pkt import util.handle_jobs as jobs def add_transitions(statemachine: Datastreamer.ServerStateController): diff --git a/Test-Rack-Software/util/serial_wrapper.py b/Test-Rack-Software/util/serial_wrapper.py index 6351d48..5be35aa 100644 --- a/Test-Rack-Software/util/serial_wrapper.py +++ b/Test-Rack-Software/util/serial_wrapper.py @@ -1,9 +1,10 @@ # This script provides a few wrapper functions for PySerial import serial # Pyserial! Not serial import serial.tools.list_ports -import util.packets as packet +import util.communication.packets as packet +import util.communication.ds_serial as serial_interface -connected_comports: list[serial.Serial] = [] +connected_comports: list[serial_interface.SerialChannel] = [] def get_com_ports(): @@ -20,22 +21,22 @@ def close_com_ports(): for port in connected_comports: port.close() -def close_port(port: serial.Serial): +def close_port(port: serial_interface.SerialChannel): """Close a specific port""" port.close() -def clear_port(port: serial.Serial): +def clear_port(port: serial_interface.SerialChannel): """Clears all of the data in the port buffer""" - port.reset_input_buffer() - port.reset_output_buffer() + port.serial_port.reset_input_buffer() + port.serial_port.reset_output_buffer() print("(clear_port) Successfully cleared port " + port.name) -def hard_reset(port: serial.Serial): +def hard_reset(port: serial_interface.SerialChannel): """Clears all of the data in the port buffer by closing the port and opening it back up""" port.close() port.open() - print("(clear_port) Successfully hard reset port " + port.name) + print("(clear_port) Successfully hard reset port " + port.serial_port.name) alr_init = False @@ -47,7 +48,7 @@ def t_init_com_ports(): global alr_init init_com_ports() if alr_init == False: - port = serial.Serial("COM8", write_timeout=0) + port = serial_interface.SerialChannel(serial.Serial("COM8", write_timeout=0)) connected_comports.append(port) alr_init = True print("(init_comports) Initialized port COM8") @@ -61,13 +62,13 @@ def init_com_ports(): print("(init_comports) Attempting to initialize all connected COM ports..") for port_data in get_com_ports(): try: - port = serial.Serial(port_data.device, write_timeout=0) + port = serial_interface.SerialChannel(serial.Serial(port_data.device, write_timeout=0)) connected_comports.append(port) print("(init_comports) Initialized port " + port_data.device) except serial.SerialException as err: if("denied" in str(err)): for connected in connected_comports: - if(connected.name == port_data.device): + if(connected.serial_port.name == port_data.device): print("(init_comports) " + port_data.device + " already initialized") else: From 29ee43fc11b93e3c3a49d6cbfc7bbdfb8d1b62b0 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Sun, 5 Nov 2023 11:59:25 -0600 Subject: [PATCH 19/24] Added test completion up to av detect --- Central-Server/API/services/datastreamer_test.py | 3 +-- Central-Server/API/services/serial_tester.py | 2 +- Test-Rack-Software/tars_rack/interface.py | 9 ++++++--- Test-Rack-Software/util/communication/packets.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Central-Server/API/services/datastreamer_test.py b/Central-Server/API/services/datastreamer_test.py index 8986241..710fe87 100644 --- a/Central-Server/API/services/datastreamer_test.py +++ b/Central-Server/API/services/datastreamer_test.py @@ -8,12 +8,11 @@ import os import time import json -import util.communication.packets as pkt +import util.packets as pkt sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) -import util.communication.packets as pkt comport = "COM9" diff --git a/Central-Server/API/services/serial_tester.py b/Central-Server/API/services/serial_tester.py index 2187006..e8cd5a4 100644 --- a/Central-Server/API/services/serial_tester.py +++ b/Central-Server/API/services/serial_tester.py @@ -9,7 +9,7 @@ sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) -import util.communication.packets as pkt +import util.packets as pkt success = 0 fails = 0 diff --git a/Test-Rack-Software/tars_rack/interface.py b/Test-Rack-Software/tars_rack/interface.py index d237301..11417c7 100644 --- a/Test-Rack-Software/tars_rack/interface.py +++ b/Test-Rack-Software/tars_rack/interface.py @@ -15,6 +15,7 @@ import time import serial import util.communication.packets as pkt +import util.communication.ds_serial as serial_interface import traceback import util.avionics_interface as AVInterface import util.datastreamer_server as Datastreamer @@ -29,13 +30,15 @@ def handle_init(self) -> None: def detect(self) -> bool: # For TARS, we need to make sure that we're already connected to the server print("(detect_avionics) Attempting to detect avionics") - if(not self.server.server_port): + if(not self.server.server_comm_channel): print("(detect_avionics) No server detected!") self.ready = False return False - - ignore_ports = [self.server.server_port] + ignore_ports = [] + # We should ignore the server's comport if the chosen server communication channel is serial.. + if type(self.server.server_comm_channel) == serial_interface.SerialChannel: + ignore_ports = [self.server.server_comm_channel] for comport in server.connected_comports: if not (comport in ignore_ports): diff --git a/Test-Rack-Software/util/communication/packets.py b/Test-Rack-Software/util/communication/packets.py index efbbab6..2d5b49d 100644 --- a/Test-Rack-Software/util/communication/packets.py +++ b/Test-Rack-Software/util/communication/packets.py @@ -212,7 +212,7 @@ def read_to_input_buffer(self, port: serial.Serial) -> None: """ Appends all current packets in the port's buffer into the DataPacketBuffer input buffer. """ - in_buffer = DataPacketBuffer.serial_to_packet_list(port) + in_buffer = DataPacketBuffer.channel_to_packet_list(port) for in_packet in in_buffer: self.input_buffer.append(in_packet) From 78a3e493be034d63117acf75bb48de9e34c386ca Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Mon, 6 Nov 2023 23:56:35 -0600 Subject: [PATCH 20/24] Added ws channel --- Test-Rack-Software/tars_rack/interface.py | 2 +- .../communication/communication_interface.py | 5 ++- .../{ds_serial.py => serial_channel.py} | 8 +++- .../util/communication/ws_channel.py | 39 +++++++++++++++++++ Test-Rack-Software/util/serial_wrapper.py | 2 +- 5 files changed, 51 insertions(+), 5 deletions(-) rename Test-Rack-Software/util/communication/{ds_serial.py => serial_channel.py} (82%) create mode 100644 Test-Rack-Software/util/communication/ws_channel.py diff --git a/Test-Rack-Software/tars_rack/interface.py b/Test-Rack-Software/tars_rack/interface.py index 11417c7..3d8b411 100644 --- a/Test-Rack-Software/tars_rack/interface.py +++ b/Test-Rack-Software/tars_rack/interface.py @@ -15,7 +15,7 @@ import time import serial import util.communication.packets as pkt -import util.communication.ds_serial as serial_interface +import util.communication.serial_channel as serial_interface import traceback import util.avionics_interface as AVInterface import util.datastreamer_server as Datastreamer diff --git a/Test-Rack-Software/util/communication/communication_interface.py b/Test-Rack-Software/util/communication/communication_interface.py index f52e99a..5c5b826 100644 --- a/Test-Rack-Software/util/communication/communication_interface.py +++ b/Test-Rack-Software/util/communication/communication_interface.py @@ -1,9 +1,12 @@ from abc import ABC, abstractmethod # ABC = Abstract Base Classes class CommunicationChannel(ABC): - is_open: bool = False """An extendable class representing a single communication channel. Exposes methods for sending string-type data, reading string-type data, and other helper methods.""" + + is_open: bool = False + """Whether this communication channel is open""" + @abstractmethod def write(self, data: str) -> None: """Writes data to the channel""" diff --git a/Test-Rack-Software/util/communication/ds_serial.py b/Test-Rack-Software/util/communication/serial_channel.py similarity index 82% rename from Test-Rack-Software/util/communication/ds_serial.py rename to Test-Rack-Software/util/communication/serial_channel.py index 7c230c2..c8460e9 100644 --- a/Test-Rack-Software/util/communication/ds_serial.py +++ b/Test-Rack-Software/util/communication/serial_channel.py @@ -8,10 +8,14 @@ def __init__(self, serial_port: serial.Serial) -> None: self.is_open = serial_port.is_open def open(self) -> None: - self.serial_port.open() + if(not self.serial_port.is_open): + self.serial_port.open() + self.is_open = True def close(self) -> None: - self.serial_port.close() + if(self.serial_port.is_open): + self.serial_port.close() + self.is_open = False def waiting_in(self) -> bool: return self.serial_port.in_waiting > 0 diff --git a/Test-Rack-Software/util/communication/ws_channel.py b/Test-Rack-Software/util/communication/ws_channel.py new file mode 100644 index 0000000..2f1926d --- /dev/null +++ b/Test-Rack-Software/util/communication/ws_channel.py @@ -0,0 +1,39 @@ +import util.communication.communication_interface as communication_interface +from websockets.sync.client import connect, ClientConnection + +class WebsocketChannel(communication_interface.CommunicationChannel): + websocket_connection: ClientConnection = None + """websocket ClientConnection which handles the basic communication layer""" + websocket_uri: str = "" + """URI stored for the websocket connection""" + + in_buffer: str = "" + """Buffer-type object storing input data for raw packets.""" + + def __init__(self, websocket_uri: str) -> None: + self.websocket_uri = websocket_uri + self.websocket_connection = connect(websocket_uri) + self.is_open = True + + def open(self) -> None: + self.websocket_connection = connect(self.websocket_uri) + self.is_open = True + + def close(self) -> None: + self.websocket_connection.close() + self.is_open = False + + def waiting_in(self) -> bool: + return len(self.in_buffer) > 0 + + def waiting_out(self): + """Websockets instantly send, this function will always return FALSE""" + return False + + def read(self) -> str: + out_temp = self.in_buffer + self.in_buffer = "" + return out_temp + + def write(self, data: str) -> None: + self.websocket_connection.send(data) diff --git a/Test-Rack-Software/util/serial_wrapper.py b/Test-Rack-Software/util/serial_wrapper.py index 5be35aa..bbe6f09 100644 --- a/Test-Rack-Software/util/serial_wrapper.py +++ b/Test-Rack-Software/util/serial_wrapper.py @@ -2,7 +2,7 @@ import serial # Pyserial! Not serial import serial.tools.list_ports import util.communication.packets as packet -import util.communication.ds_serial as serial_interface +import util.communication.serial_channel as serial_interface connected_comports: list[serial_interface.SerialChannel] = [] From 1432c00b5ed79ddf2af4e1a8bcd6d6e11d387e36 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Tue, 7 Nov 2023 00:07:03 -0600 Subject: [PATCH 21/24] half baked:: added prio communication channels, doesn't do anything yet. --- Test-Rack-Software/config.py | 6 ++++++ Test-Rack-Software/main.py | 9 +++++++++ .../util/communication/communication_interface.py | 8 ++++++++ 3 files changed, 23 insertions(+) diff --git a/Test-Rack-Software/config.py b/Test-Rack-Software/config.py index d0abd9d..20e94e4 100644 --- a/Test-Rack-Software/config.py +++ b/Test-Rack-Software/config.py @@ -1,6 +1,7 @@ # This file is not a "traditional" configuration file, it instead provides runtime symbolic links to a # defined avionics interface. This means that by defining which stack you want to use in the # next couple of lines, this file automatically tells `main.py` which file to access. +from util.communication.communication_interface import CommunicationChannelType # Metadata needs to be imported first, so the avionics system knows what data to use for startup. # Import all metaconfigs @@ -19,6 +20,11 @@ # == EDIT THE VARIABLE BELOW TO CHANGE WHICH INTERFACE IS USED == use_interface = TARSmkIV """Which interface should this testing rack use?""" +# ===================== OTHER CONFIGURATION ===================== +preferred_communication_channel = CommunicationChannelType.SERIAL +"""The preferred channel of communication between the datastreamer and server. This method will be tried first. Defaults to `SERIAL`""" + + # =============================================================== diff --git a/Test-Rack-Software/main.py b/Test-Rack-Software/main.py index 54c9d71..f9e1972 100644 --- a/Test-Rack-Software/main.py +++ b/Test-Rack-Software/main.py @@ -45,6 +45,9 @@ def should_heartbeat(Server: Datastreamer.DatastreamerServer): # Server connection def send_wide_ident(Server: Datastreamer.DatastreamerServer): if(time.time() > Server.last_server_connection_check): + + # Check websockets + connection.t_init_com_ports() for port in connection.connected_comports: packet.DataPacketBuffer.write_packet(packet.CL_IDENT(av_meta.board_type), port) @@ -74,7 +77,9 @@ def detect_avionics(Server: Datastreamer.DatastreamerServer): try: return avionics.av_instance.detect() except Exception as e: + print("(detect_avionics) Detect_avionics encountered an error during the detection process:") print(e) + print() def on_ready(Server: Datastreamer.DatastreamerServer): @@ -117,6 +122,10 @@ def handle_power_cycle(Server: Datastreamer.DatastreamerServer): def main(): Server = Datastreamer.instance + # Server config setup + Server.server_preferred_comm_method = test_board_config.preferred_communication_channel # Sets up the priority communication channel + + # END Server config setup SState = Datastreamer.ServerStateController.ServerState # SState alias # Make sure setup is done before any transition: diff --git a/Test-Rack-Software/util/communication/communication_interface.py b/Test-Rack-Software/util/communication/communication_interface.py index 5c5b826..e87d7b9 100644 --- a/Test-Rack-Software/util/communication/communication_interface.py +++ b/Test-Rack-Software/util/communication/communication_interface.py @@ -1,4 +1,12 @@ from abc import ABC, abstractmethod # ABC = Abstract Base Classes +from enum import Enum + +class CommunicationChannelType(Enum): + """Enum storing all supported communication types. Communication type must be present here to be able to be prioritized""" + SERIAL = 0 + """Serial interface (Wired: COM)""" + WEBSOCKET = 1 + """Websocket interface (Wireless: ws://)""" class CommunicationChannel(ABC): """An extendable class representing a single communication channel. Exposes methods for From a15f07bc6922059705eb81d656d543040d3b8ce8 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 8 Nov 2023 15:02:51 -0600 Subject: [PATCH 22/24] final datastreamer touches --- Test-Rack-Software/tars_rack/interface.py | 13 +++++++++---- Test-Rack-Software/util/datastreamer_server.py | 4 ++++ Test-Rack-Software/util/handle_packets.py | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Test-Rack-Software/tars_rack/interface.py b/Test-Rack-Software/tars_rack/interface.py index 3d8b411..4506c9d 100644 --- a/Test-Rack-Software/tars_rack/interface.py +++ b/Test-Rack-Software/tars_rack/interface.py @@ -38,12 +38,13 @@ def detect(self) -> bool: ignore_ports = [] # We should ignore the server's comport if the chosen server communication channel is serial.. if type(self.server.server_comm_channel) == serial_interface.SerialChannel: + print("(detect_avionics) Server is using Serial Channel interface") ignore_ports = [self.server.server_comm_channel] for comport in server.connected_comports: if not (comport in ignore_ports): - print("(detect_avionics) Detected viable avionics target @ " + comport.name) - self.TARS_port = comport + print("(detect_avionics) Detected viable avionics target @ " + comport.serial_port.name) + self.TARS_port = comport.serial_port self.ready = True return True @@ -141,10 +142,14 @@ def job_setup(self): return False, "Abort signal recieved" try: + if(self.av_interface.TARS_port.is_open): + return True, "Setup Complete" self.av_interface.TARS_port.open() - print("\n(job_setup) Successfully re-opened TARS port (" + self.av_interface.TARS_port.name + ")") + print("\n(job_setup) Successfully re-opened TARS port (" + self.av_interface.TARS_port.serial_port.name + ")") return True, "Setup Complete" - except: + except Exception as e: + print(e) + print("") time_left = abs((start + 10) - time.time()) print(f"(job_setup) attempting to re-open tars port.. ({time_left:.1f}s)", end="\r") return False, "Unable to re-open avionics COM Port" diff --git a/Test-Rack-Software/util/datastreamer_server.py b/Test-Rack-Software/util/datastreamer_server.py index 8631b6b..9b6dd4b 100644 --- a/Test-Rack-Software/util/datastreamer_server.py +++ b/Test-Rack-Software/util/datastreamer_server.py @@ -200,8 +200,12 @@ class DatastreamerServer: state: ServerStateController = ServerStateController() """Datastreamer server's state machine reference""" board_type: str = "" + + # COMMUNICATION server_comm_channel: communication_interface.CommunicationChannel = None """Serial port reference to the serial port that connects to the Kamaji server.""" + server_preferred_comm_method: communication_interface.CommunicationChannelType = communication_interface.CommunicationChannelType.SERIAL + """The preferred method for datastreamer boards to communicate to Kamaji. Defaults to `SERIAL`""" board_id = -1 """Board ID assigned by the Kamaji server""" diff --git a/Test-Rack-Software/util/handle_packets.py b/Test-Rack-Software/util/handle_packets.py index dea979a..1077db5 100644 --- a/Test-Rack-Software/util/handle_packets.py +++ b/Test-Rack-Software/util/handle_packets.py @@ -25,7 +25,7 @@ def handle_server_packets(Server: Datastreamer.DatastreamerServer): Server.packet_buffer.add(pkt.CL_PONG()) case pkt.DataPacketType.ACKNOWLEDGE: # For an invalid ACK, we send back an invalid packet. - if(Server.server_port != None): + if(Server.server_comm_channel != None): Server.packet_buffer.add(pkt.CL_INVALID(packet)) case pkt.DataPacketType.REASSIGN: # For REASSIGN, we check if the command is valid, reassign if yes, INVALID if no. From d1bdaf59f5c33e94cdb499d9cf69181bdf50e8e1 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 8 Nov 2023 15:10:44 -0600 Subject: [PATCH 23/24] documentation changes --- Test-Rack-Software/README.md | 9 ++++++++- Test-Rack-Software/util/README.md | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Test-Rack-Software/README.md b/Test-Rack-Software/README.md index d5ac3df..1a6b428 100644 --- a/Test-Rack-Software/README.md +++ b/Test-Rack-Software/README.md @@ -55,7 +55,14 @@ The way that peripherals are implemented and detected is left as an exercise to As of now, all connections between systems are made with serial, this may be modified later on to allow for more board peripherals (since one of the serial ports is used for server communication). -#### Packets +#### Communication + +##### Communication interfaces +Datastreamer provides an interface for implementing serial-like communication interfaces through the `./util/communication/communication_interface.py` interface. The definition of "serial-like" communication is quite abstract, but in short, the communication protocol you are trying to implement must be able to send and recieve strings in some manner. Whether the strings are encoded or not matters not, you must simply be able to implement `read()`, which will return a `str`, and `write(data: str)`, which takes in a string. The other methods are helper methods to work with string buffers. + +To see a simple example of a communication interface, take a look at `./util/communication/serial_channel.py`. + +##### Packets The datastreamer communicates to the Kamaji server through a system of packets. The purpose of each packet is well documented within `packets.py`, so it is recommended to check out what each packet does. In general, each packet has a **packet header** and an optional **raw_data** parameter. The packet header is encoded using JSON, while `raw_data` is usually encoded as a UTF-8 string. Packets are encoded to the serial buffer in the following manner as a string: `{packet_header}[[raw==>]]{raw_data}[[pkt_end]]`. This string can then be decoded back into the relevant packet, assuming that no data corruption happens. diff --git a/Test-Rack-Software/util/README.md b/Test-Rack-Software/util/README.md index 90e002b..7f1a008 100644 --- a/Test-Rack-Software/util/README.md +++ b/Test-Rack-Software/util/README.md @@ -20,9 +20,18 @@ A helper file for handling all job-related functions (Hooks into datastreamer fu `handle_packets.py` A helper file for handling all server packets (Hooks into datastreamer functionality) -`packets.py` +`communication/packets.py` Establishes packet communication protocol for datastreamer. +`communication/communication_interface.py` +Provides communication channel interfaces to be implemented for communication with Kamaji. + +`communication/serial_channel.py` +Communication channel class for Serial (USB) communication + +`communication/ws_channel.py` +Communication channel class for websocket communication + `pio_commands.py` Wrapper for **platformio** From 87b40961452b165d239500837019b361189e1457 Mon Sep 17 00:00:00 2001 From: Michael Karpov Date: Wed, 8 Nov 2023 15:12:14 -0600 Subject: [PATCH 24/24] Removed nginx pid for merge --- nginx/logs/nginx.pid | 1 - 1 file changed, 1 deletion(-) delete mode 100644 nginx/logs/nginx.pid diff --git a/nginx/logs/nginx.pid b/nginx/logs/nginx.pid deleted file mode 100644 index f19a05e..0000000 --- a/nginx/logs/nginx.pid +++ /dev/null @@ -1 +0,0 @@ -13304