def _thrust_monitor(self): # recalculate accumulated delta-v so far self.accumulated_delta_v = self._calculate_accumulated_delta_v() current_velocity = get_telemetry("orbitalVelocity") #print("Accumulated dV: {:.2f}".format(self.accumulated_delta_v)) #print("dV required: {:.2f}".format(self.delta_v_required)) #print("Velocity at start: {:.2f}".format(self.initial_speed)) #print("Current Velocity: {:.2f}".format(current_velocity)) #print("Expected dV at cutoff: {}".format(self.velocity_at_cutoff)) if current_velocity > (self.velocity_at_cutoff - 13.5) and not self._is_thrust_reduced: utils.log("Throttling back to 10%", log_level="DEBUG") telemachus.set_throttle(10) self._is_thrust_reduced = True telemachus.disable_smartass() telemachus.send_command_to_ksp("command=f.sas") if current_velocity > (self.velocity_at_cutoff - 3.5): # the 3.5 a hack otherwise it overshoots, FIXME! telemachus.cut_throttle() utils.log("Closing throttle, burn complete!", log_level="DEBUG") computer.dsky.current_verb.terminate() computer.execute_verb(verb="06", noun="14") computer.main_loop_table.remove(self._thrust_monitor) #computer.burn_complete() self.terminate() computer.go_to_poo()
def get_telemetry(data, body_number=None): """ Contacts telemachus for the requested data. :param data: The API call required :type data: str | float :param body_number: Specify which body to obtain data for :type body_number: string :rtype: string """ # if telemetry is None: # raise TelemetryNotAvailable try: query_string = data + "=" + telemetry[data] except KeyError: raise KSPNotConnected return if body_number: query_string += "[{}]".format(body_number) try: raw_response = urllib.request.urlopen(config.URL + query_string) except urllib.error.URLError: utils.log("Query string: {}".format(query_string), log_level="ERROR") utils.log("Caught exception urllib2.URLERROR", log_level="ERROR") raise KSPNotConnected response_string = raw_response.read().decode("utf-8)") json_response = json.loads(response_string) return json_response[data]
def _accept_enable_engine(self, data): if data == "proceed": utils.log("Go for burn!", log_level="INFO") else: return computer.main_loop_table.append(self._fine_start_time_monitor) computer.execute_verb(verb="16", noun="40")
def handle_data_register_load(): """ Handles data register loading :return: None """ if keypress.isdigit() == False: utils.log( "Expecting a digit for data load, got {}".format(keypress), log_level="ERROR") return display_register = state["display_location_to_load"] if state["register_index"] == 0: if keypress == "+": dsky.set_register("+", display_register) elif keypress == "-": dsky.set_register("-", display_register) else: dsky.set_register("b", display_register) state["register_index"] += 1 if 1 <= state["register_index"] <= 5: dsky.set_register(keypress, display_register, digit=state["register_index"]) if state["register_index"] >= 5: state["register_index"] = 0 else: state["register_index"] += 1 state["input_data_buffer"] += keypress
def execute(self): """ Executes the program. :return: None """ super().execute() utils.log("Program 11 executing", log_level="INFO") # test if KSP is connected if check_connection() == False: return # --> call average G integration with ΔV integration # self.computer.run_average_g_routine = True # --> terminate gyrocompassing if "02" in self.computer.running_programs: self.computer.programs["02"].terminate() # --> compute initial state vector # self.computer.routines["average_g"]() # --> Display on DSKY: # --> V06 N62 (we are going to use V16N62 though, so we can have a updated display # --> R1: Velocity # --> R2: Rate of change of vehicle altitude # --> R3: Vehicle altitude in km to nearest .1 km self.computer.execute_verb(verb="16", noun="62")
def _fine_start_time_monitor(self): self.time_until_ignition = self.calculate_time_to_ignition() if float(self.time_until_ignition) < 1.1: # ADJUSTED FROM 0.1 to 1.1 to dry fix start delay of approx 1 second utils.log("Engine Ignition", log_level="INFO") self._begin_burn() computer.main_loop_table.remove(self._fine_start_time_monitor)
def send_command_to_ksp(command_string): try: urllib.request.urlopen(config.URL + command_string) except urllib.error.URLError: utils.log("Query string: {}".format(command_string), log_level="ERROR") utils.log("Caught exception urllib2.URLERROR", log_level="ERROR") raise KSPNotConnected
def receive_data(self, data): """ Allows the verb to receive requested data from the DSKY. :param data: the data sent by DSKY """ utils.log("{} received data: {}".format(self, data)) self.data = data self.execute()
def on(self): ''' Turns the IMU on :returns: True if successful, False otherwise ''' if check_connection() == False: utils.log("Cannot connect to KSP", "WARNING") else: self.set_coarse_align()
def set_annunciator(self, name, set_to=True): try: if set_to: self.annunciators[name].on() else: self.annunciators[name].off() except KeyError: utils.log("You tried to change a annunciator that doesnt exist :(", "WARNING")
def stop_blink(self): """ THIS METHOD IS DEPRECIATED. It is left in here to catch any other usages. Stops the verb/noun flash :return: None """ utils.log(message="Called depreciated method stop_blink(). Please use verb_noun_flash_off()", log_level="ERROR")
def disable_direction_autopilot(self): """ Disables the directional autopilot :return: None """ telemachus.disable_smartass() self.is_direction_autopilot_engaged = False utils.log("Autopilot disabled", log_level="INFO")
def execute(self): """ Executes the program. :return: None """ utils.log("Executing Program {}: {}".format(self.number, self.description)) self.computer.flash_comp_acty(500) self.computer.dsky.set_register(self.number, "program") self.computer.running_program = self
def check_for_liftoff(self): if get_telemetry("verticalSpeed") > 1: utils.log("Liftoff discrete") Program.computer.remove_from_mainloop(self.check_for_liftoff) # Clear display for register in ["verb", "noun", "program", "data_1", "data_2", "data_3"]: Program.computer.dsky.blank_register(register) # pause for 1 second, then run P11 self.timer.start(1000)
def operator_error(self, message=None): """ Called when the astronaut has entered invalid keyboard input. :param message: Optional message to send to log :return: None """ if message: utils.log("OPERATOR ERROR: " + message, log_level="ERROR") self.dsky.annunciators["opr_err"].blink_timer.start(500)
def stop_blink(self): """ THIS METHOD IS DEPRECIATED. It is left in here to catch any other usages. Stops the verb/noun flash :return: None """ utils.log( message= "Called depreciated method stop_blink(). Please use verb_noun_flash_off()", log_level="ERROR")
def _send_output(self): """ Sends the requested output to the DSKY """ # check if the display update interval needs to be changed if self.timer.interval() != config.DISPLAY_UPDATE_INTERVAL: # stop and start the timer to change the update interval self.timer.stop() self.timer.start(config.DISPLAY_UPDATE_INTERVAL) if self.noun is None: self.noun = Verb.computer.keyboard_state["requested_noun"] if self.noun in self.illegal_nouns: raise NounNotAcceptableError noun_function = Verb.computer.nouns[self.noun]() try: data = noun_function.return_data() except nouns.NounNotImplementedError: self.computer.operator_error("Noun {} not implemented yet. Sorry about that...".format(self.noun)) self.terminate() return except KSPNotConnected: utils.log("KSP not connected, terminating V{}".format(self.number), log_level="ERROR") Verb.computer.program_alarm(110) self.terminate() raise except TelemetryNotAvailable: utils.log("Telemetry not available, terminating V{}".format(self.number), log_level="ERROR") Verb.computer.program_alarm(111) self.terminate() raise if not data: # if the noun returns False, the noun *should* have already raised a program alarm, so we just need to # terminate and return self.terminate() return output = self._format_output_data(data) # set tooltips if not self.is_tooltips_set: Verb.computer.dsky.set_tooltip("data_1", data["tooltips"][0]) Verb.computer.dsky.set_tooltip("data_2", data["tooltips"][1]) Verb.computer.dsky.set_tooltip("data_3", data["tooltips"][2]) self.is_tooltips_set = True # display data on DSKY registers Verb.computer.dsky.set_register(output[0], "data_1") Verb.computer.dsky.set_register(output[1], "data_2") Verb.computer.dsky.set_register(output[2], "data_3") Verb.computer.dsky.flash_comp_acty()
def check_for_liftoff(self): if get_telemetry("verticalSpeed") > 1: utils.log("Liftoff discrete") Program.computer.remove_from_mainloop(self.check_for_liftoff) # Clear display for register in [ "verb", "noun", "program", "data_1", "data_2", "data_3" ]: Program.computer.dsky.blank_register(register) # pause for 1 second, then run P11 self.timer.start(1000)
def display(self, number_to_display): # cast to string (but should be a string already, so log it if not isinstance(number_to_display, str): utils.log("You should pass a string to be displayed bu GUI!") number_to_display = str(number_to_display) # only change image if we arn't flashing, it will be changed next flash if self.blink_data["is_blinking"] == False: image = self.digit_pixmaps[number_to_display] self.setPixmap(image) self.blink_data["blink_value"] = number_to_display
def program_restart(self, alarm_code, message=None): """ Triggers a program restart. :param alarm_code: a 3 or 4 digit octal int of the alarm code to raise :param message: optional message to print to log :return: None """ # TODO: insert terminate and restart program utils.log("Program fresh start not implemented yet... watch this space...") if message: utils.log(message, log_level="ERROR")
def set_coarse_align(self): ''' Sets coarse align mode. :returns: None ''' self.is_fine_aligned = False self.is_course_aligned = True self.computer.dsky.set_annunciator("no_att") if self.update_gyro_angles in self.computer.main_loop_table: self.computer.main_loop_table.remove(self.update_gyro_angles) if self.check_for_gimbal_lock in self.computer.main_loop_table: self.computer.main_loop_table.remove(self.check_for_gimbal_lock) utils.log("IMU coarse align set")
def computer_restart(self, alarm_code, message=None): """ Triggers a guidance computer hardware restart. The most severe of the errors! :param alarm_code: a 3 or 4 digit octal int of the alarm code to raise :param message: optional message to print to log :return: None """ # insert computer reboot # self.fresh_start() if message: utils.log(message, log_level="CRITICAL") pass
def execute(self): """ Executes the verb :return: """ if self.noun in self.illegal_nouns: raise NounNotAcceptableError utils.log("Executing Verb {}: {}".format(self.number, self.name)) self.dsky.current_verb = self if self.noun: Verb.computer.dsky.set_register(self.noun, "noun") utils.log(" With noun {}".format(self.noun)) # JRI
def program_alarm(self, alarm_code): """ Sets the program alarm codes in memory and turns the PROG annunciator on. :param alarm_code: a 3 or 4 digit octal int of the alarm code to raise :return: None """ utils.log("PROGRAM ALARM {}: {}".format(str(alarm_code), config.ALARM_CODES[alarm_code]), log_level="ERROR") alarm_code += 1000 if self.alarm_codes[0] != 0: self.alarm_codes[1] = self.alarm_codes[0] self.alarm_codes[0] = alarm_code self.alarm_codes[2] = self.alarm_codes[0] self.dsky.annunciators["prog"].on()
def return_data(self): utils.log("Noun 09 requested") alarm_codes = computer.alarm_codes data = { 1: str(alarm_codes[0]), 2: str(alarm_codes[1]), 3: str(alarm_codes[2]), "is_octal": True, "tooltips": [ "First alarm code", "Second alarm code", "Last alarm code", ], } return data
def request_data(self, requesting_object, display_location, is_proceed_available=False): """ Requests data entry from the user. :param requesting_object: the object requesting the data :param display_location: the register that entered data will be displayed in :param is_proceed_available: True if the user can key in PROCEED instead of data :return: None """ # if is_proceed_available is set True, don't blank display as it should be showing data to proceed with if not is_proceed_available: self.blank_register(display_location) utils.log("{}, method {}() requesting data".format(requesting_object.__self__, requesting_object.__name__)) self.verb_noun_flash_on() self.computer.keyboard_state["object_requesting_data"] = requesting_object self.computer.keyboard_state["is_expecting_data"] = True self.computer.keyboard_state["display_location_to_load"] = display_location
def set_fine_align(self): ''' Sets fine align mode. :returns: None ''' # if no connection to KSP, stop fine align and go back to coarse align if check_connection() == False: utils.log("IMU: cannot complete fine align, no connection to KSP", log_level="ERROR") return self.is_fine_aligned = True self.is_course_aligned = False self.computer.dsky.set_annunciator("gimbal_lock", False) self.computer.dsky.set_annunciator("no_att", False) #self.computer.main_loop_table.append(self.update_gyro_angles) #self.computer.main_loop_table.append(self.check_for_gimbal_lock) utils.log("IMU fine align set")
def terminate(self): """ Terminates the verb :return: None """ utils.log("Terminating V{}".format(self.number)) Verb.computer.dsky.stop_annunciator_blink("key_rel") Verb.computer.keyboard_state["display_lock"] = None Verb.computer.keyboard_state["backgrounded_update"] = None self.timer.stop() self.noun = None # self.activity_timer.Stop() # reset tooltips to "" Verb.computer.dsky.set_tooltip("data_1", "") Verb.computer.dsky.set_tooltip("data_2", "") Verb.computer.dsky.set_tooltip("data_3", "")
def poodoo_abort(self, alarm_code, message=None): """ Terminates the faulty program, and executes Program 00 (P00) :param alarm_code: a 3 digit octal int of the alarm code to raise :return: None """ # alarm_message = config.ALARM_CODES[alarm_code] alarm_code += 2000 if self.alarm_codes[0] != 0: self.alarm_codes[1] = self.alarm_codes[0] self.alarm_codes[0] = alarm_code self.alarm_codes[2] = self.alarm_codes[0] self.dsky.annunciators["prog"].on() self.running_program.terminate() utils.log("P00DOO ABORT {}: {}".format(str(alarm_code), message), log_level="ERROR") poo = self.programs["00"]() poo.execute()
def handle_expected_data(): """ Handles expected data entry. :return: None """ #set_trace() if keypress == "P": dsky.verb_noun_flash_off() utils.log("Proceeding without input, calling {}".format( str(state["object_requesting_data"]))) state["object_requesting_data"]("proceed") state["input_data_buffer"] = "" state["is_expecting_data"] = False return # if we receive ENTER, the load is complete and we will call the # program or verb requesting the data load elif keypress == "E": input_data = state["input_data_buffer"] state["input_data_buffer"] = "" state["is_expecting_data"] = False dsky.verb_noun_flash_off() data_requester = state["object_requesting_data"] utils.log("Data load complete, calling {}({})".format( data_requester.__self__, data_requester.__name__)) data_requester(input_data) return if state["display_location_to_load"] in ["verb", "noun", "program"]: handle_control_register_load() else: handle_data_register_load() # if the user as entered anything other than a numeric d, # trigger a OPR ERR and recycle program if keypress.isalpha(): # if a program is running, recycle it # INSERT TRY HERE!!! # computer.get_state("running_program").terminate() # INSERT EXCEPT HERE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # if a verb is running, recycle it # computer.get_state("running_verb").terminate() computer.operator_error("Expecting numeric input") return
def execute(self): self.calculate() time_to_node = HohmannTransfer.get_time_to_node(self.phase_angle_difference(), self.orbital_period, self.departure_body_period) if time_to_node <= 120: utils.log("Time of ignition less that 2 minutes in the future, starting burn during next orbit") time_to_node += self.orbital_period self.calculate_burn_timings() self.first_burn = Burn(delta_v=self.delta_v_1, direction="node", time_of_ignition=self.time_of_ignition_first_burn, time_of_node=self.time_of_node, burn_duration=self.duration_of_burn, ) if config.current_log_level == "DEBUG": self.print_maneuver_data() computer.add_burn(self.first_burn)
def phase_angle_difference(self): #set_trace() current_phase_angle = get_telemetry("body_phaseAngle", body_number=self.target_id) phase_angle_difference = current_phase_angle - self.phase_angle_required #if phase_angle_difference < 0: #phase_angle_difference = 180 + abs(phase_angle_difference) #utils.log("Adding 180 degrees to phase angle difference") utils.log() utils.log("Current Phase Angle: {} degrees".format(current_phase_angle)) utils.log("Phase Angle Required: {} degrees".format(self.phase_angle_required)) utils.log("Phase Angle Difference: {} degrees".format(phase_angle_difference)) return phase_angle_difference
def handle_expected_data(): """ Handles expected data entry. :return: None """ #set_trace() if keypress == "P": dsky.verb_noun_flash_off() utils.log("Proceeding without input, calling {}".format(str(state["object_requesting_data"]))) state["object_requesting_data"]("proceed") state["input_data_buffer"] = "" state["is_expecting_data"] = False return # if we receive ENTER, the load is complete and we will call the # program or verb requesting the data load elif keypress == "E": input_data = state["input_data_buffer"] state["input_data_buffer"] = "" state["is_expecting_data"] = False dsky.verb_noun_flash_off() data_requester = state["object_requesting_data"] utils.log("Data load complete, calling {}({})".format(data_requester.__self__, data_requester.__name__)) data_requester(input_data) return if state["display_location_to_load"] in ["verb", "noun", "program"]: handle_control_register_load() else: handle_data_register_load() # if the user as entered anything other than a numeric d, # trigger a OPR ERR and recycle program if keypress.isalpha(): # if a program is running, recycle it # INSERT TRY HERE!!! # computer.get_state("running_program").terminate() # INSERT EXCEPT HERE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # if a verb is running, recycle it # computer.get_state("running_verb").terminate() computer.operator_error("Expecting numeric input") return
def execute(self): ''' Executes the program :returns: None ''' super().execute() # if TIG < 2 mins away, abort burn if utils.seconds_to_time(self.burn.time_until_ignition)["minutes"] < 2: self.computer.remove_burn() self.computer.poodoo_abort(226) return # if time to ignition if further than a hour away, display time to ignition if utils.seconds_to_time(self.burn.time_until_ignition)["hours"] > 0: utils.log("TIG > 1 hour away") self.computer.execute_verb(verb="16", noun="33") self.computer.main_loop_table.append(self._ten_minute_monitor) else: utils.log("TIG < 1 hour away, enabling burn") self.burn.execute()
def request_data(self, requesting_object, display_location, is_proceed_available=False): """ Requests data entry from the user. :param requesting_object: the object requesting the data :param display_location: the register that entered data will be displayed in :param is_proceed_available: True if the user can key in PROCEED instead of data :return: None """ # if is_proceed_available is set True, don't blank display as it should be showing data to proceed with if not is_proceed_available: self.blank_register(display_location) utils.log("{}, method {}() requesting data".format( requesting_object.__self__, requesting_object.__name__)) self.verb_noun_flash_on() self.computer.keyboard_state[ "object_requesting_data"] = requesting_object self.computer.keyboard_state["is_expecting_data"] = True self.computer.keyboard_state[ "display_location_to_load"] = display_location
def handle_data_register_load(): """ Handles data register loading :return: None """ if keypress.isdigit() == False: utils.log("Expecting a digit for data load, got {}".format(keypress), log_level="ERROR") return display_register = state["display_location_to_load"] if state["register_index"] == 0: if keypress == "+": dsky.set_register("+", display_register) elif keypress == "-": dsky.set_register("-", display_register) else: dsky.set_register("b", display_register) state["register_index"] += 1 if 1 <= state["register_index"] <= 5: dsky.set_register(keypress, display_register, digit=state["register_index"]) if state["register_index"] >= 5: state["register_index"] = 0 else: state["register_index"] += 1 state["input_data_buffer"] += keypress
def execute(self): """ Executes the verb. :return: None """ if Verb.computer.keyboard_state["backgrounded_update"]: utils.log("Terminating backgrounded update") Verb.computer.keyboard_state["backgrounded_update"].terminate() Verb.computer.dsky.stop_annunciator_blink("key_rel") if Verb.computer.running_program: utils.log("Terminating active program {}".format(Verb.computer.running_program.number)) Verb.computer.running_program.terminate() else: utils.log("V34 called, but nothing to terminate!")
def _enable_directional_autopilot(self): telemachus.set_mechjeb_smartass("node") utils.log("Directional autopilot enabled", log_level="INFO") return True
def calc_burn_duration(initial_mass, thrust, specific_impulse, delta_v): ''' Calculates the duration of a burn in seconds. :param initial_mass: initial mass of spacecraft :type initial_mass: float :param thrust: total thrust of the spacecraft :type thrust: float :param specific_impulse: Isp :type specific_impulse: int or float :param delta_v: delta_v for burn :type delta_v: float :returns: float time of burn in seconds ''' exhaust_velocity = specific_impulse * 9.81 burn_duration = (initial_mass * exhaust_velocity / thrust) * (1 - math.exp(-delta_v / exhaust_velocity)) utils.log(log_level="info") utils.log("-" * 40, log_level="info") utils.log("Burn duration calculations:", log_level="info") utils.log("Initial mass: {} tonnes".format(initial_mass), log_level="info") utils.log("Thrust: {} kN".format(thrust), log_level="info") utils.log("Specific Impulse: {} seconds".format(specific_impulse), log_level="info") utils.log("Exhaust Velocity: {:.2f} kg/s".format(exhaust_velocity), log_level="info") utils.log("Burn Duration: {:.1f} seconds".format(burn_duration), log_level="info") utils.log("-" * 40, log_level="info") return burn_duration
def calculate_other_parameters(self): utils.log("Initial velocity at start of transfer: {:.2f} m/s".format(self.calculate_velocity_initial())) utils.log("Velocity on transfer orbit at initial orbit: {:.2f} m/s".format(self.calculate_velocity_initial_on_transfer_orbit())) utils.log("Velocity on transfer orbit at final orbit: {:.2f} m/s".format(self.calculate_velocity_final_on_transfer_orbit())) utils.log("Initial velocity change (delta-v): {:.2f} m/s".format(self.calculate_initial_delta_v())) utils.log("Final velocity change (delta-v): {:.2f} m/s".format(self.calculate_final_delta_v())) utils.log("Total chance in velocity (delta-v): {:.2f} m/s".format(self.calculate_total_delta_v())) utils.log()
def set_register(self, value, register, digit=None): ''' Displays some data on a register. :param value: the value to display :type value: str :param register: the register to display it on :type register: str :param digit: if set, indicates that just one digit is to be set :type digit: str :returns: False if value checks fail, True if display set sucessfully ''' # some sanity checks. If checks fail, return False # if digit is set, only should be one digit supplied if digit: if len(value) != 1: utils.log("You are trying to display a single digit, but got {} digits instead!".format(len(value))) return False # if register is a control register, should have either 1 or 2 values to display else: if register in ["verb", "noun", "program"]: if not 1 <= len(value) <= 2: utils.log("You are trying to display a value in the {} register, expecting 1 or 2 digits, " \ "got {}".format(register, len(value))) return False else: # otherwise, check for value being length 1 to 6 if not 1 <= len(value) <= 6: # set_trace() utils.log("Must have between 1 and 6 values to display in data register, got {}".format(len(value))) return False # also check that the first digit is either a "+", "-" or "b" (for blank) if value[0] not in ["+", "-", "b"]: utils.log("First digit to display should be either +, -, or b, got {}".format(value[0])) return False # setting control register if register in ["verb", "noun", "program"]: this_register = self.get_register(register) # get the register we want to change if digit is not None: this_register[digit].display(value) else: for index in range(len(value)): this_register[str(index + 1)].display(value[index]) return True # setting data register else: display_map = { 0: "sign", 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", } # data register 1 if register == "data_1": this_register = self.registers["data"]["1"] # get the register we want to change if digit: this_register[str(digit)].display(value) else: for index in range(len(value)): digit_to_set = display_map[index] value_to_set = value[index] this_register[digit_to_set].display(value_to_set) elif register == "data_2": this_register = self.registers["data"]["2"] # get the register we want to change if digit: this_register[str(digit)].display(value) else: for index in range(len(value)): digit_to_set = display_map[index] value_to_set = value[index] this_register[digit_to_set].display(value_to_set) elif register == "data_3": this_register = self.registers["data"]["3"] # get the register we want to change if digit: this_register[str(digit)].display(value) else: for index in range(len(value)): digit_to_set = display_map[index] value_to_set = value[index] this_register[digit_to_set].display(value_to_set)