├── examples └── ksp-pynet │ ├── ksppynet │ ├── __init__.py │ ├── ksppynet.py │ ├── flight_plan_node.py │ └── flight_plan.py │ ├── kpconsole.kv │ └── main.py ├── README.md ├── LICENSE └── Art_Whaleys_KRPC_Demos ├── Art_Whaleys_KRPC_Demos.pyproj ├── docking_autopilot.py ├── pid.py ├── node_executor.py ├── rover.py ├── rendezvous.py ├── Simple_Launch_Script.py └── landing.py /examples/ksp-pynet/ksppynet/__init__.py: -------------------------------------------------------------------------------- 1 | """The ksppynet package""" 2 | from .ksppynet import * 3 | from .flight_plan import * 4 | 5 | __all__ = (ksppynet.__all__, 6 | flight_plan.__all__) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Community Code Library for kRPC 2 | 3 | This is a library of code examples for use with the [kRPC 4 | mod](https://github.com/krpc/krpc) for Kerbal Space Program, developed by the 5 | kRPC community. 6 | 7 | ## Contributing 8 | 9 | If you would like to contribute to the library, please feel free to open a pull request! 10 | 11 | ## Licensing 12 | 13 | Code contributed to this repository is under the MIT license. The act of 14 | submitting content constitutes implied consent to have the content placed under the 15 | MIT license. 16 | 17 | The full license terms can be found in the LICENSE file alongside this readme, 18 | but in summary, you are free to share, re-use and modify any of the code as long 19 | as you (1) give attribution that links back to here; and (2) don't sue. No 20 | warranty is given, or liability assumed. 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 kRPC Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/Art_Whaleys_KRPC_Demos.pyproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | 2.0 6 | {6afe5bee-de58-43d5-9762-8a90b3f8a0db} 7 | 8 | 9 | 10 | . 11 | . 12 | {888888a0-9f3d-457c-b088-3a5042f75d52} 13 | Standard Python launcher 14 | {2af0f10d-7135-4994-9156-5d01c9c11b7e} 15 | 2.7 16 | 17 | 18 | 19 | 20 | 10.0 21 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Code 30 | 31 | 32 | Code 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/docking_autopilot.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | ### Docking Autopilot Library and Example 3 | ###################################################################### 4 | ### Like all of the scripts in my folder here, this file contains 5 | ### functions you might want to include into your own scripts for 6 | ### actual use and a demo in the 'main' function that you can just 7 | ### run to see how it works. 8 | ### 9 | ### This file demonstrates an automatic docking. It assumes that 10 | ### your vessel is already on the correct side of the vessel to dock 11 | ### to. In the near future I'll work on code to allow it to clear 12 | ### the target vessel safely and put itself on the correct side, but 13 | ### for now it's a great and fairly simple example of using some 14 | ### basic PID methods to control vessels with precision. 15 | ###################################################################### 16 | 17 | import time 18 | import krpc 19 | import collections 20 | import math 21 | 22 | from pid import PID 23 | 24 | v3 = collections.namedtuple('v3', 'right forward up') 25 | 26 | ############################################################################## 27 | ## Main - only run when this file is explicitly executed 28 | ############################################################################## 29 | def main(): 30 | conn = krpc.connect() 31 | dock(conn) 32 | 33 | ############################################################################## 34 | ## dock - The actually interesting function in this file. 35 | ## works by lining vessel up parallel, with 10m of separation between 36 | ## docking ports. When this is confirmed, it moves forward slowly to dock. 37 | ############################################################################## 38 | def dock(conn, speed_limit = 1.0): 39 | 40 | #Setup KRPC 41 | sc = conn.space_center 42 | v = sc.active_vessel 43 | t = sc.target_docking_port 44 | ap = v.auto_pilot 45 | rf = v.orbit.body.reference_frame 46 | 47 | #Setup Auto Pilot 48 | ap.reference_frame = rf 49 | ap.target_direction = tuple(x * -1 for x in t.direction(rf)) 50 | ap.engage() 51 | 52 | #create PIDs 53 | upPID = PID(.75,.25,1) 54 | rightPID = PID(.75,.25,1) 55 | forwardPID = PID(.75,.2,.5) 56 | 57 | proceed=False 58 | #'proceed' is a flag that signals that we're lined up and ready to dock. 59 | # Otherwise the program will try to line up 10m from the docking port. 60 | 61 | #LineUp and then dock - in the same loop with 'proceed' controlling whether 62 | #we're headed for the 10m safe point, or headed forward to dock. 63 | while True: 64 | offset = getOffsets(v, t) #grab data and compute setpoints 65 | velocity = getVelocities(v, t) 66 | if proceedCheck(offset): #Check whether we're lined up and ready to dock 67 | proceed = True 68 | setpoints = getSetpoints(offset, proceed, speed_limit) 69 | 70 | upPID.setpoint(setpoints.up) #set PID setpoints 71 | rightPID.setpoint(setpoints.right) 72 | forwardPID.setpoint(setpoints.forward) 73 | 74 | v.control.up = -upPID.update(velocity.up) #steer vessel 75 | v.control.right = -rightPID.update(velocity.right) 76 | v.control.forward = -forwardPID.update(velocity.forward) 77 | 78 | time.sleep(.1) 79 | 80 | ############################################################################## 81 | ## Helper Functions 82 | ############################################################################## 83 | def getOffsets(v, t): 84 | ''' 85 | returns the distance (right, forward, up) between docking ports. 86 | ''' 87 | return v3._make(t.part.position(v.parts.controlling.reference_frame)) 88 | 89 | def getVelocities(v, t): 90 | ''' 91 | returns the relative velocities (right, forward, up) 92 | ''' 93 | return v3._make(v.velocity(t.reference_frame)) 94 | 95 | def getSetpoints(offset, proceed, speed_limit): 96 | ''' 97 | returns the computed set points - 98 | set points are actually just the offset distances clamped to the 99 | speed_limit variable! This way we slow down as we get closer to the right 100 | heading. 101 | ''' 102 | tvup = max(min(offset.up, speed_limit), -speed_limit) 103 | tvright = -1 * (max(min(offset.right, speed_limit), -speed_limit)) 104 | if proceed: 105 | tvforward = -.2 106 | else: 107 | tvforward = max(min((10 - offset.forward), speed_limit), -speed_limit) 108 | return v3(tvright, tvforward, tvup) 109 | 110 | def proceedCheck(offset): 111 | ''' 112 | returns true if we're lined up and ready to move forward to dock. 113 | ''' 114 | return (offset.up < .1 and 115 | offset.right < .1 and 116 | math.fabs(10 - offset.forward)<.1) 117 | 118 | 119 | 120 | ##This calls the main function which is at the top of the file for readability's sake! 121 | if __name__ == '__main__': 122 | main() 123 | print('--') 124 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/pid.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | ### PID Controller Library and Example 3 | ############################################################################## 4 | ### Like all of the scripts in my folder here, this file contains 5 | ### functions you might want to include into your own scripts for 6 | ### actual use and a demo in the 'main' function that you can just 7 | ### run to see how it works. 8 | ### 9 | ### This file includes a simple and generic PID controller that we use in 10 | ### many of the other examples when we want to smoothly control one value 11 | ### based on our measurement of another. The PID class docstring explains 12 | ### the basics of using it in your project. The demo code below that 13 | ### shows how to use the PID controller to hold a vertical velocity with 14 | ### variation of engine thrust. 15 | ############################################################################## 16 | 17 | import time 18 | import krpc 19 | 20 | class PID(object): 21 | ''' 22 | Generic PID Controller Class 23 | Based on the PID recipe at : 24 | 25 | http://code.activestate.com/recipes/577231-discrete-pid-controller/ 26 | 27 | and the code and discussions in the blog at: 28 | 29 | http://brettbeauregard.com/blog/2011/04/ 30 | improving-the-beginners-pid-introduction/ 31 | 32 | An instance is created with the format 33 | your_pid=PID(P=.0001, I=0.00001, D=0.000001) 34 | 35 | Finding the right values for those three gain numbers is called 'tuning' and 36 | that's beyond the scope of this doc string! 37 | 38 | Use your_pid.setpoint(X) to set the target output value of the controller. 39 | 40 | Regularly call your_pid.update(Y), passing it the input data that the 41 | controller should respond to. 42 | output_data = your_pid.update(input_data) 43 | 44 | ''' 45 | 46 | def __init__(self, P=1.0, I=0.1, D=0.01): 47 | self.Kp = P #P controls reaction to the instantaneous error 48 | self.Ki = I #I controls reaction to the history of error 49 | self.Kd = D #D prevents overshoot by considering rate of change 50 | self.P = 0.0 51 | self.I = 0.0 52 | self.D = 0.0 53 | self.SetPoint = 0.0 #Target value for controller 54 | self.ClampI = 1.0 #clamps i_term to prevent 'windup.' 55 | self.LastTime = time.time() 56 | self.LastMeasure = 0.0 57 | 58 | def update(self,measure): 59 | now = time.time() 60 | change_in_time = now - self.LastTime 61 | if not change_in_time: 62 | change_in_time = 1.0 #avoid potential divide by zero if PID just created. 63 | 64 | error = self.SetPoint - measure 65 | self.P = error 66 | self.I += error 67 | self.I = self.clamp_i(self.I) # clamp to prevent windup lag 68 | self.D = (measure - self.LastMeasure) / (change_in_time) 69 | 70 | self.LastMeasure = measure # store data for next update 71 | self.lastTime = now 72 | 73 | return (self.Kp * self.P) + (self.Ki * self.I) - (self.Kd * self.D) 74 | 75 | def clamp_i(self, i): 76 | if i > self.ClampI: 77 | return self.ClampI 78 | elif i < -self.ClampI: 79 | return -self.ClampI 80 | else: 81 | return i 82 | 83 | def setpoint(self, value): 84 | self.SetPoint = value 85 | self.I = 0.0 86 | 87 | ############################################################################## 88 | ## Demo Code Below This Line! 89 | ############################################################################## 90 | 91 | Target_Velocity = 5 # The value we're trying to limit ourselves to 92 | 93 | ############################################################################## 94 | ## Main -- only run when we execute this file directly. 95 | ## ignored when we import the PID into other files! 96 | ############################################################################## 97 | def main(): 98 | #Setup KRPC 99 | conn = krpc.connect() 100 | sc = conn.space_center 101 | v = sc.active_vessel 102 | telem = v.flight(v.orbit.body.reference_frame) 103 | 104 | # Create PID controller. 105 | p = PID(P=.25, I=0.025, D=0.0025) 106 | p.ClampI = 20 107 | p.setpoint(Target_Velocity) 108 | 109 | # starting with locked SAS and throttle at full 110 | v.control.sas = True 111 | v.control.throttle = 1.0 112 | while not v.thrust: # stage if we just launched a new rocket 113 | v.control.activate_next_stage() 114 | 115 | # Loop Forever, or until you get the point of this example and stop it. 116 | while True: 117 | the_pids_output=p.update(telem.vertical_speed) 118 | v.control.throttle=the_pids_output 119 | print('Vertical V:{:03.2f} PID returns:{:03.2f} Throttle:{:03.2f}' 120 | .format(telem.vertical_speed, 121 | the_pids_output, 122 | v.control.throttle)) 123 | time.sleep(.1) 124 | 125 | 126 | 127 | if __name__ == '__main__': 128 | main() 129 | print('--') 130 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/node_executor.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | ### Node Execution Library and Example 3 | ###################################################################### 4 | ### Like all of the scripts in my folder here, this file contains 5 | ### functions you might want to include into your own scripts for 6 | ### actual use and a demo in the 'main' function that you can just 7 | ### run to see how it works. 8 | ### 9 | ### This file shows how to execute maneuver nodes. The docstring 10 | ### for the execute_next_node function explains how to make this work! 11 | ### And you can see an example of doing so in the launch script. 12 | ###################################################################### 13 | 14 | import krpc 15 | import math 16 | import time 17 | 18 | def main(): 19 | conn = krpc.connect() 20 | #Demo of all three major functions in this file - uncomment the one you want! 21 | execute_btn(conn) #Creates an on screen button to execute the next node 22 | # execute_next_node(conn) #Executes the next node! 23 | # execute_all_nodes(conn) #executes ALL nodes instead of just the next one! 24 | 25 | def execute_next_node(conn): 26 | ''' 27 | This is the actually interesting function in this script! 28 | 29 | Executes the Next Maneuver Node for the vessel provided. 30 | If you just open and run this file, it will execute a node and exit. 31 | You can also include this file into your own script with the line 32 | 33 | from node_executor import execute_next_node 34 | 35 | at the top of your script, and then anytime you want to execute a node 36 | you just have to call (execute_next_node(conn) passing it the active 37 | KRPC connection as a parameter. 38 | 39 | I'm also demonstrating two different ways to point the vessel with the 40 | autopilot. One relies on the vessel having SAS Node holding capabilty, 41 | the other uses the KRPC built-in auto-pilot. The one built into 42 | KRPC can require some tuning depending on your vessel... but works on 43 | any vessel regardless of pilot skill/probe core choice! 44 | ''' 45 | space_center = conn.space_center 46 | vessel = space_center.active_vessel 47 | ap=vessel.auto_pilot 48 | 49 | # Grab the next node if it exists 50 | try: 51 | node = vessel.control.nodes[0] 52 | except Exception: 53 | return #Fail silently but gracefully if there was no node to execute 54 | 55 | 56 | # Orient vessel to the node 57 | ################## One Way To Orient Vessel!############## 58 | rf = vessel.orbit.body.reference_frame 59 | ap.reference_frame=rf 60 | ap.engage() 61 | ap.target_direction = node.remaining_burn_vector(rf) 62 | ap.wait() 63 | 64 | ################## Another Way To Orient Vessel!######## 65 | #ap.sas = True 66 | #time.sleep(.1) 67 | #ap.sas_mode = vessel.auto_pilot.sas_mode.maneuver 68 | #ap.wait() 69 | 70 | # Calculate the length and start of burn 71 | m = vessel.mass 72 | isp = vessel.specific_impulse 73 | dv = node.delta_v 74 | F = vessel.available_thrust 75 | G = 9.81 76 | burn_time = (m - (m / math.exp(dv / (isp * G)))) / (F / (isp * G)) 77 | 78 | # Warp until burn 79 | space_center.warp_to(node.ut - (burn_time / 2.0) - 5.0) 80 | while node.time_to > (burn_time / 2.0): 81 | pass 82 | ap.wait() 83 | 84 | # Actually Burn 85 | vessel.control.throttle = thrust_controller(vessel, node.remaining_delta_v) 86 | while node.remaining_delta_v > .1: 87 | ap.target_direction=node.remaining_burn_vector(rf)#comment out this line 88 | #if using the vessel sas method to orient vessel 89 | vessel.control.throttle = thrust_controller(vessel, node.remaining_delta_v) 90 | 91 | # Finish Up 92 | ap.disengage() 93 | vessel.control.throttle = 0.0 94 | node.remove() 95 | 96 | def execute_all_nodes(conn): 97 | 98 | ''' 99 | as the name implies - this function executes ALL maneuver nodes currently 100 | planned for the vessel in series. 101 | ''' 102 | space_center = conn.space_center 103 | vessel = space_center.active_vessel 104 | while vessel.control.nodes: 105 | execute_next_node(conn) 106 | 107 | def thrust_controller(vessel, deltaV): 108 | ''' 109 | This function is somewhat arbitrary in it's working - there's not a 'rule' 110 | that I've found on how to feather out throttle towards the end of a burn 111 | but given that the chances of overshooting go up with TWR (since we fly 112 | in a world with discrete physics frames!) it makes sense to relate the 113 | throttle to the TWR for this purpose. 114 | ''' 115 | TWR= vessel.max_thrust/vessel.mass 116 | if deltaV < TWR / 3: 117 | return .05 118 | elif deltaV < TWR / 2: 119 | return .1 120 | elif deltaV < TWR: 121 | return .25 122 | else: 123 | return 1.0 124 | 125 | def execute_btn(conn): 126 | ''' 127 | Demo of how to use the UI Service to turn this node execution function into 128 | a handy little utility for doing something useful. Just puts a button on 129 | the screen. When you click it - it executes the next maneuver node. 130 | ''' 131 | space_center = conn.space_center 132 | vessel = space_center.active_vessel 133 | canvas = conn.ui.stock_canvas # draw on the main screen 134 | panel = canvas.add_panel() #container for our button 135 | rect = panel.rect_transform #rect to define panel 136 | rect.size = (100, 30) #panel size 137 | rect.position = (110-(canvas.rect_transform.size[0]/2), 0) #left middle 138 | button = panel.add_button("Execute Node") #add the button 139 | button.rect_transform.position = (0, 20) #locate the button 140 | button_clicked = conn.add_stream(getattr, button, 'clicked') #watch button 141 | while True: #if button clicked, execute the next node 142 | if button_clicked(): 143 | execute_next_node(conn) 144 | button.clicked = False 145 | 146 | # ---------------------------------------------------------------------------- 147 | # Activate main loop, if we are executing THIS file explicitly. 148 | # ---------------------------------------------------------------------------- 149 | if __name__ == "__main__" : 150 | main() 151 | -------------------------------------------------------------------------------- /examples/ksp-pynet/kpconsole.kv: -------------------------------------------------------------------------------- 1 | #:kivy 1.0 2 | # 3 | # kpconsole.kv: Layout of the console for ksp-pynet. 4 | # 5 | 6 | : 7 | size: (160, 50) 8 | text_size: self.size 9 | halign: "center" 10 | valign: "middle" 11 | size_hint: (None, None) 12 | 13 | : 14 | size: (300, 30) 15 | text_size: self.size 16 | halign: "right" 17 | valign: "middle" 18 | size_hint: (None, None) 19 | font_name: 'DejaVuSans' 20 | 21 | : 22 | size: (200, 30) 23 | multiline: False 24 | size_hint: (None, None) 25 | 26 | : 27 | size: (200, 30) 28 | multiline: False 29 | size_hint: (None, None) 30 | 31 | # 32 | # MenuActive: Displayed when the application is connected to KSP via KRPC. 33 | # 34 | : 35 | ScrollView: 36 | GridLayout: 37 | cols: 1 38 | size_hint_y: None 39 | height: self.minimum_height 40 | spacing: 10 41 | 42 | MenuButtonTemplate: 43 | text: 'Diagnostics' 44 | on_press: app.screen.current = 'diags' 45 | 46 | MenuButtonTemplate: 47 | text: 'Flight Planner' 48 | on_press: app.screen.current = 'flight_plan' 49 | 50 | MenuButtonTemplate: 51 | text: 'Browser' 52 | on_press: app.screen.current = 'browser' 53 | 54 | MenuButtonTemplate: 55 | text: 'Viewer' 56 | on_press: 57 | app.screen.current = 'viewer' 58 | app.console.display.viewer.register(app, True) 59 | 60 | MenuButtonTemplate: 61 | text: 'Maneuver' 62 | on_press: app.screen.current = 'maneuver' 63 | 64 | # 65 | # Menu: Displayed when the application is not connected to KSP. 66 | # 67 | : 68 | connection: connection 69 | dynamic: dynamic 70 | alerts: alerts 71 | 72 | BoxLayout: 73 | orientation: 'vertical' 74 | padding: 10 75 | spacing: 20 76 | 77 | MenuButtonTemplate: 78 | id: connection 79 | text: 'Connection' 80 | background_color_normal: '' 81 | background_color: (1,0.8,0.5,1) 82 | on_press: app.screen.current = "connection" 83 | 84 | BoxLayout: 85 | orientation: 'vertical' 86 | id: dynamic 87 | size_hint_y: 7 88 | 89 | ScrollView: 90 | padding: 20 91 | size_hint_y: 2 92 | width: 200 93 | size_hint_x: None 94 | Label: 95 | id: alerts 96 | markup: True 97 | font_name: 'RobotoMono-Regular' 98 | color: (1,1,0,0.2) 99 | size_hint_y: None 100 | text_size: self.width, None 101 | height: self.texture_size[1] 102 | text: "Welcome to KSP Pynet" 103 | 104 | # 105 | # Display*: Frame templates for displaying. 106 | # 107 | # DisplayDiagnostics: Frame for displaying diagnostics. 108 | # 109 | : 110 | BoxLayout: 111 | orientation: 'vertical' 112 | padding: 50 113 | spacing: 20 114 | 115 | ScrollView: 116 | id: scrlv 117 | size_hint_y: 7 118 | TextInput: 119 | id: diag_text 120 | font_name: 'RobotoMono-Regular' 121 | size_hint_y: None 122 | text_size: self.width, None 123 | height: max(self.height, scrlv.height) 124 | text: "Diagnostics to appear here.." 125 | 126 | BoxLayout: 127 | orientation: 'horizontal' 128 | size_hint_y: 1 129 | 130 | MenuButtonTemplate: 131 | text: 'Test 1' 132 | on_press: app.pynet_request(None, "test") 133 | 134 | MenuButtonTemplate: 135 | text: 'Test 2' 136 | on_press: app.pynet_request(None, "test 2") 137 | 138 | # 139 | # DisplayFlightPlan: Frame for displaying/setting flight plan parameters 140 | # 141 | : 142 | GridLayout: 143 | cols: 2 144 | spacing: 10 145 | 146 | DefaultLabel: 147 | text: "Launch Parameters:" 148 | DefaultLabel: 149 | 150 | DefaultLabel: 151 | id: heading 152 | text: "Heading" 153 | state: "90" 154 | DefaultFloatInput: 155 | text: heading.state 156 | DefaultLabel: 157 | id: target_altitude 158 | text: "Target Launch Altitude" 159 | state: "95000" 160 | DefaultFloatInput: 161 | text: target_altitude.state 162 | 163 | DefaultLabel: 164 | text: "Orbtial Parameters:" 165 | DefaultLabel: 166 | 167 | DefaultLabel: 168 | id: apoapsis 169 | text: "Apoapsis" 170 | state: "100000" 171 | DefaultFloatInput: 172 | text: apoapsis.state 173 | DefaultLabel: 174 | id: periapsis 175 | text: "Periapsis" 176 | state: "100000" 177 | DefaultFloatInput: 178 | text: periapsis.state 179 | 180 | DefaultLabel: 181 | DefaultLabel: 182 | DefaultLabel: 183 | MenuButtonTemplate: 184 | halign: "center" 185 | text: "Go" 186 | on_press: root.go(app) 187 | 188 | # 189 | # DisplayConnection: Frame for displaying/setting connection details. 190 | # 191 | : 192 | connected: connected 193 | connected_state: connected_state 194 | GridLayout: 195 | cols: 2 196 | spacing: 10 197 | DefaultLabel: 198 | id: ip 199 | text: "IP address" 200 | state: "127.0.0.1" 201 | DefaultTextInput: 202 | text: ip.state 203 | DefaultLabel: 204 | id: port 205 | text: "Port" 206 | state: "50000" 207 | DefaultTextInput: 208 | text: port.state 209 | DefaultLabel: 210 | id: connected_state 211 | state: str(False) 212 | text: "" 213 | MenuButtonTemplate: 214 | id: connected 215 | text: "Connect" 216 | background_color: (0.5,1,0.5,0.8) 217 | on_press: root.go(app) 218 | 219 | # 220 | # DisplayConnection: Frame for displaying/setting maneuvers. 221 | # Note that most of the dynamic elements of this can be found in the .py. 222 | # 223 | : 224 | GridLayout: 225 | cols: 3 226 | spacing: 10 227 | 228 | # 229 | # Viewer: Frame for the planet viewer. 230 | # 231 | : 232 | body_name: "Kerbin" 233 | Widget: 234 | Label: 235 | size: root.size 236 | halign: "right" 237 | valign: "top" 238 | font_size: '30sp' 239 | text: root.body_name 240 | 241 | # 242 | # Display: Frame to contain all display frames 243 | # 244 | : 245 | sm: sm 246 | viewer: viewer 247 | dconn: dconn 248 | RelativeLayout: 249 | ScreenManager: 250 | id: sm 251 | 252 | Screen: 253 | name: "disconnected" 254 | 255 | Screen: 256 | name: "connection" 257 | BoxLayout: 258 | orientation: 'vertical' 259 | DefaultLabel: 260 | text: "KRPC Connection Parameters:" 261 | DisplayConnection: 262 | id: dconn 263 | cols: 2 264 | 265 | Screen: 266 | name: "flight_plan" 267 | BoxLayout: 268 | orientation: 'vertical' 269 | 270 | DisplayFlightPlan: 271 | cols: 2 272 | 273 | Screen: 274 | name: "diags" 275 | DisplayDiagnostics: 276 | 277 | Screen: 278 | name: "viewer" 279 | canvas: 280 | Color: 281 | rgba: (0.1, 0.1, 0.2, 1) 282 | Rectangle: 283 | pos: self.pos 284 | size: self.size 285 | Viewer: 286 | id: viewer 287 | 288 | Screen: 289 | name: "maneuver" 290 | DisplayManeuver: 291 | cols: 3 292 | 293 | Screen: 294 | name: "browser" 295 | 296 | # 297 | # Console: The parent widget for everything containing the display and a menu. 298 | # 299 | : 300 | display: display 301 | menu: menu 302 | 303 | BoxLayout: 304 | canvas.before: 305 | Color: 306 | rgba: (0.2, 0.2, 0.2, 1) 307 | Rectangle: 308 | pos: self.pos 309 | size: self.size 310 | orientation: 'horizontal' 311 | padding: 20 312 | spacing: 20 313 | 314 | Menu: 315 | id: menu 316 | width: 180 317 | size_hint_x: None 318 | 319 | Display: 320 | id: display 321 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/rover.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | ### Rover Controller Library and Example 3 | ###################################################################### 4 | ### Like all of the scripts in my folder here, this file contains 5 | ### functions you might want to include into your own scripts for 6 | ### actual use and a demo in the 'main' function that you can just 7 | ### run to see how it works. 8 | ### 9 | ### This file shows how to make a rover navigate to a waypoint. 10 | ### The function has a few advanced features - most interestingly 11 | ### it autosaves at a fixed interval, if the rover is stable and 12 | ### moving and seems to be intact. This is an effort to fix the 13 | ### frustration of having a rover crash when you're an hour from 14 | ### your last quick save. 15 | ###################################################################### 16 | 17 | import krpc 18 | import time 19 | import math 20 | from collections import namedtuple 21 | from math import atan2,degrees,cos,sqrt,asin,sin,radians 22 | 23 | from pid import PID #imports my PID controller from the PID example file! 24 | 25 | latlon = namedtuple('latlon', 'lat lon') #create a named tuple 26 | 27 | ############################################################################## 28 | ## Main function - this exists to demonstrate the use of the library. If 29 | ## you import this library into your own script, main doesn't get called. 30 | ############################################################################## 31 | def main(): 32 | conn = krpc.connect() 33 | 34 | #Create a waypoint to drive to 35 | wp1 = conn.space_center.waypoint_manager.add_waypoint( 36 | 0.05,-75.0, conn.space_center.active_vessel.orbit.body,"Waypoint1") 37 | 38 | #call the rover autopilot 39 | rover_go(conn, wp1) 40 | 41 | #remove the waypoint when the function returns 42 | wp1.remove() 43 | 44 | ############################################################################## 45 | ## And here's the function that's actually interesting! 46 | ############################################################################## 47 | def rover_go(conn, waypoint, speed = 10.0, savetime = 300 ): 48 | ''' 49 | Function to drive a rover to the specified waypoint. Must be called with 50 | an active KRPC connection and a valid waypoint. Attempts to bring rover 51 | to a complete stop and quicksave a file called "rover_ap" at a regular 52 | interval. Defaults to 5 minutes. This and rover speed can be specified 53 | as optional arguments. A savetime of 0 turns this feature off. 54 | ''' 55 | 56 | ## grab hold of the krpc functions we'll need to drive the rover 57 | v=conn.space_center.active_vessel 58 | ground_telem=v.flight(v.orbit.body.reference_frame) 59 | surf_telem=v.flight(v.surface_reference_frame) 60 | target=latlon(waypoint.latitude, waypoint.longitude) 61 | 62 | autosave.lastsave = time.time() 63 | partslist = v.parts.all 64 | there_yet = False; 65 | 66 | ## Setup the PID controllers for steering and throttle. The steering 67 | ## setpoint is locked to 0 since we'll be feeding in an error number to 68 | ## the update function. 69 | steering= PID(.01,.01,.001) 70 | throttle = PID(.5,.01,.001) 71 | steering.setpoint(0) 72 | throttle.setpoint(speed) 73 | 74 | 75 | #The main loop that drives to the way point 76 | while not there_yet: 77 | 78 | autosave(conn, savetime, partslist) ##call autosave to see if we should save yet 79 | recharge(conn) 80 | 81 | ## Steering control - handles selecting a bearing, comparing it to 82 | ## current heading and feeding that error in degrees to the PID to 83 | ## get the control correction required. 84 | location = latlon(ground_telem.latitude, ground_telem.longitude) 85 | target_heading = heading_for_latlon(target, location) 86 | course_correct= course_correction(surf_telem.heading, target_heading) 87 | steer_correct = steering.update(course_correct) 88 | v.control.wheel_steering= steer_correct 89 | 90 | #Throttle control - tries to maintain the given speed! 91 | v.control.brakes = False 92 | throttsetting= throttle.update(ground_telem.speed) 93 | v.control.wheel_throttle = throttsetting 94 | 95 | #Check if we're there to end the loop 96 | if distance(target, location, v.orbit.body) < 50: 97 | there_yet=True 98 | 99 | ############################################################################## 100 | ### Autosave function. Saves if the vessel appears stable and isn't already 101 | ### stopped, pitched greater than 30 degrees, or showing a different part 102 | ### count than before. 103 | ############################################################################## 104 | 105 | def autosave(conn, savetime, partslist): 106 | if savetime == 0: 107 | return 108 | if time.time() - autosave.lastsave > savetime: 109 | v=conn.space_center.active_vessel 110 | telem=v.flight(v.orbit.body.reference_frame) 111 | 112 | if safetosave(conn , partslist): 113 | v.control.throttle = 0.0 ## Stop the rover then save 114 | v.control.brakes = True 115 | while telem.speed > 0.01: 116 | pass 117 | time.sleep(.1) 118 | conn.space_center.save('rover_ap') 119 | v.control.brakes = False 120 | autosave.lastsave = time.time() 121 | 122 | # function called by autosave to determine if it's safe to save the file! 123 | # tries to avoid overwriting a good save with one we take AFTER the rover 124 | # has crashed or flipped or run into a space tree. 125 | def safetosave(conn, partslist): 126 | v=conn.space_center.active_vessel 127 | ground_telem=v.flight(v.orbit.body.reference_frame) 128 | surf_telem=v.flight(v.surface_reference_frame) 129 | 130 | if ground_telem.speed < .1: # We might be stuck! 131 | print("stuck") 132 | return False 133 | if surf_telem.pitch > 25 or surf_telem.roll >25: # We might have rolled! 134 | print("roll") 135 | return False 136 | if len(partslist) is not len(v.parts.all): # We might have lost something? 137 | print("broken") 138 | return False 139 | return True # all good! 140 | 141 | ############################################################################## 142 | ## Battery Charging Function - if the batteries are below 5% - stops rover 143 | ## and deploys solar panels until charge is above 85% then resumes travel. 144 | ############################################################################## 145 | def recharge(conn): 146 | vessel = conn.space_center.active_vessel 147 | telem=vessel.flight(vessel.orbit.body.reference_frame) 148 | Max_EC = vessel.resources.max('ElectricCharge') 149 | EC = vessel.resources.amount('ElectricCharge') 150 | if EC / Max_EC < .05: #less than 5% charge - Stop the rover 151 | vessel.control.wheel_throttle = 0 152 | vessel.control.brakes = True 153 | while telem.speed > 0.01: 154 | pass 155 | vessel.control.solar_panels = True 156 | while EC / Max_EC < .85: #less than 85% charge 157 | Max_EC = vessel.resources.max('ElectricCharge') 158 | EC = vessel.resources.amount('ElectricCharge') 159 | vessel.control.solar_panels = False ##pack up and get moving again 160 | vessel.control.brakes = False 161 | 162 | 163 | ############################################################################## 164 | ## Navigation Math Functions 165 | ############################################################################## 166 | def heading_for_latlon(target, location): 167 | 168 | lat1 = math.radians(location.lat) 169 | lat2 = math.radians(target.lat) 170 | 171 | diffLong = math.radians(target.lon - location.lon) 172 | 173 | x = math.sin(diffLong) * math.cos(lat2) 174 | y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) 175 | * math.cos(lat2) * math.cos(diffLong)) 176 | 177 | initial_bearing = math.atan2(x, y) 178 | 179 | initial_bearing = math.degrees(initial_bearing) 180 | compass_bearing = (initial_bearing + 360) % 360 181 | 182 | return compass_bearing 183 | 184 | def distance(target, location, body): 185 | R = body.equatorial_radius # Earth radius in kilometers 186 | 187 | dLat = radians(target.lat - location.lat) 188 | dLon = radians(target.lon - location.lon) 189 | lat1 = radians(location.lat) 190 | lat2 = radians(target.lat) 191 | 192 | a = sin(dLat/2)**2 + cos(lat1)*cos(lat2)*sin(dLon/2)**2 193 | c = 2*asin(sqrt(a)) 194 | 195 | return R * c 196 | 197 | def course_correction(myheading, targetbearing): 198 | unadjusted= targetbearing-myheading 199 | if unadjusted < -180: 200 | return unadjusted +360 201 | if unadjusted > 180: 202 | return unadjusted -360 203 | return unadjusted 204 | 205 | 206 | # ---------------------------------------------------------------------------- 207 | # Activate main loop, if we are executing THIS file explicitly. 208 | # ---------------------------------------------------------------------------- 209 | if __name__ == "__main__" : 210 | main() 211 | -------------------------------------------------------------------------------- /examples/ksp-pynet/ksppynet/ksppynet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import asyncio 3 | import ksppynet.flight_plan as flight_plan 4 | from threading import Thread 5 | import krpc 6 | import queue 7 | import traceback 8 | import os 9 | 10 | __all__ = ( 11 | "Pynet", 12 | ) 13 | 14 | testing_ui_on = False 15 | 16 | class PynetException(Exception): 17 | pass 18 | 19 | class PynetHandler(object): 20 | # Main thread 21 | def __init__(self): 22 | print("Setting up message queues and event loop") 23 | self.requests = queue.Queue() 24 | self.replies = queue.Queue() 25 | self.fp = None 26 | 27 | self.methods = { 28 | "connect" : self.D_connect, 29 | "disconnect" : self.D_disconnect, 30 | "plan" : self.D_plan, 31 | "orbital" : self.D_orbital, 32 | "maneuver" : self.D_maneuver, 33 | } 34 | if testing_ui_on: 35 | self.methods = { 36 | "orbital" : self.D_test_orbital, 37 | } 38 | 39 | def default_method(self, *args, **kwargs): 40 | if not hasattr(self, "test_counter"): 41 | self.test_counter = 0 42 | self.test_counter += 1 43 | self.message("Sleep for {}".format(self.test_counter), 44 | self.test_counter) 45 | return self.test_counter 46 | 47 | 48 | def D_test_orbital(self, on): 49 | self.test_counter += 1 50 | pm = PynetMessage("orbital_notify") 51 | pm.set_result({"body_name": "Earth", 52 | "body_radius" : 500000, 53 | "real_apoapsis" : 500000 + 10000 * self.test_counter, 54 | "real_periapsis" : 400000}) 55 | self.replies.put(pm) 56 | 57 | def D_maneuver(self, **kwargs): 58 | node = self.fp.get_node() 59 | mfns = { "apoapsis" : node.change_apoapsis, 60 | "periapsis" : node.change_periapsis } 61 | for k, v in kwargs.items(): 62 | self.fp.msg("Changing {} to {}".format(k, v)) 63 | mfns[k](v) 64 | self.loop.create_task(node.execute()) 65 | 66 | @asyncio.coroutine 67 | def send_orbital_params(self): 68 | self.sending_orbital = True 69 | while self.sending_orbital: 70 | yield from asyncio.sleep(0.2) 71 | pm = PynetMessage("orbital_notify") 72 | pm.set_result({"body_name" : self.fp.vessel.orbit.body.name, 73 | "body_radius" : self.fp.vessel.orbit.body.equatorial_radius, 74 | "real_apoapsis" : self.fp.attr["real_apoapsis"](), 75 | "real_periapsis" : self.fp.attr["real_periapsis"]()}) 76 | self.replies.put(pm) 77 | 78 | 79 | def D_orbital(self, on): 80 | if on: 81 | self.loop.create_task(self.send_orbital_params()) 82 | else: 83 | self.sending_orbital = False 84 | 85 | 86 | def D_plan(self, heading=0, target_altitude=75000, 87 | apoapsis=90000, periapsis=90000): 88 | self.fp.set_autostaging() 89 | 90 | self.fp.add_sequence("pre_launch", heading=heading) 91 | self.fp.add_sequence("launch", altitude=target_altitude) 92 | self.fp.add_sequence("orbit", apoapsis=apoapsis, periapsis=periapsis) 93 | self.fp.add_sequence("quit") 94 | self.fp.launch() 95 | 96 | def D_disconnect(self): 97 | self.disconnect(self) 98 | 99 | def D_connect(self, ip=None, port=None): 100 | if self.fp: 101 | raise PynetException("Already connected") 102 | conn = krpc.connect(name="{}.{}".format("KSP-PyNet", os.getpid())) 103 | vessel = conn.space_center.active_vessel 104 | self.fp = flight_plan.FlightPlan(conn, vessel, self.message) 105 | return "Connected with IP {}, PORT {}".format(ip, port) 106 | 107 | def message(self, msg, duration=10): 108 | pm = PynetMessage("message") 109 | pm.set_result({"msg" : msg, "duration" : duration}) 110 | self.replies.put(pm) 111 | 112 | def debug(self, msg): 113 | pm = PynetMessage("debug") 114 | pm.set_result({"msg" : msg}) 115 | self.replies.put(pm) 116 | 117 | def disconnect(self): 118 | if self.fp: 119 | self.fp.close() 120 | self.fp = None 121 | self.replies.put(PynetMessage("disconnect")) 122 | 123 | @asyncio.coroutine 124 | def dispatch(self, req, *args, **kwargs): 125 | try: 126 | if req in self.methods: 127 | result = self.methods[req](*args, **kwargs) 128 | else: 129 | result = self.default_method(*args, **kwargs) 130 | except Exception as e: 131 | result = {"error" : str(traceback.format_exc()) } 132 | if not isinstance(result, dict): 133 | result = {"result" : result } 134 | return result 135 | 136 | @asyncio.coroutine 137 | def request_handler(self): 138 | print("Started request handler") 139 | while True: 140 | try: 141 | msg = self.requests.get(False) # Don't block 142 | except queue.Empty: 143 | yield 144 | else: 145 | print("Got request") 146 | msg.result = yield from self.dispatch(msg.req, *msg.args, **msg.kwargs) 147 | print("Sending reply: {}".format(msg.result)) 148 | self.replies.put(msg) 149 | if msg.req == "disconnect": 150 | print("Disconnecting PyNet") 151 | self.loop.stop() 152 | return 153 | 154 | def exception_handler(self, loop, context): 155 | print(context["message"]) 156 | if (not "exception" in context 157 | or not isinstance(context["exception"], ConnectionResetError)): 158 | self.loop.default_exception_handler(context) 159 | self.debug("Hit exception:\n{}".format(context["message"])) 160 | self.loop.stop() 161 | 162 | def run(self, loop): 163 | self.fp = None 164 | asyncio.set_event_loop(loop) 165 | self.loop = asyncio.get_event_loop() 166 | print("Pynet running") 167 | # Create a task to handle incoming requests then wait for 168 | # all tasks to complete. 169 | self.loop.set_exception_handler(self.exception_handler) 170 | 171 | # Start the request handler 172 | self.loop.create_task(self.request_handler()) 173 | 174 | pending = asyncio.Task.all_tasks() 175 | try: 176 | self.loop.run_until_complete(asyncio.gather(*pending)) 177 | except RuntimeError: 178 | print("Pynet Loop stopped, disconnected") 179 | 180 | self.disconnect() 181 | 182 | # Main thread 183 | def start_thread(self): 184 | loop = asyncio.get_event_loop() 185 | self.t = Thread(target=self.run, args=(loop,)) 186 | self.t.start() 187 | 188 | # Main thread 189 | def join(self): 190 | if self.t: 191 | self.t.join() 192 | return 193 | 194 | # Main thread 195 | def put(self, msg): 196 | self.requests.put(msg) 197 | 198 | # Main thread 199 | def iter_results(self): 200 | empty = False 201 | while not empty: 202 | try: 203 | item = self.replies.get(False) 204 | yield item 205 | except queue.Empty: 206 | empty = True 207 | 208 | class PynetMessage(object): 209 | def __init__(self, req, *args, **kwargs): 210 | self.req = req 211 | self.args = args 212 | self.kwargs = kwargs 213 | self.result = None 214 | 215 | def set_result(self, result_dict): 216 | self.result = result_dict 217 | 218 | class Pynet(object): 219 | def __init__(self): 220 | self.pynet_handler = None 221 | self.callbacks = {} 222 | self.default_callback = None 223 | 224 | def connect(self, connection_callback, 225 | ip=None, port=None, 226 | default_callback=None): 227 | self.callbacks = {} 228 | if not self.pynet_handler: 229 | self.pynet_handler = PynetHandler() 230 | print("starting Pynet thread") 231 | self.pynet_handler.start_thread() 232 | self.default_callback = default_callback 233 | self.register("connect", connection_callback) 234 | self.register("disconnect", connection_callback) 235 | self.send_async("connect", ip=ip, port=port) 236 | 237 | def disconnect(self): 238 | if self.pynet_handler: 239 | self.send_async("disconnect") 240 | self.pynet_handler.join() 241 | # Drain the queue of replies before fully disconnecting. 242 | self.recv_async() 243 | self.pynet_handler = None 244 | self.default_callback = None 245 | self.callbacks = {} 246 | 247 | 248 | def register(self, req, callback): 249 | self.callbacks[req] = callback 250 | 251 | def recv_async(self): 252 | if self.pynet_handler: 253 | for res in self.pynet_handler.iter_results(): 254 | if res.req in self.callbacks: 255 | self.callbacks[res.req](res.req, res.result) 256 | elif self.default_callback: 257 | self.default_callback(res.req, res.result) 258 | else: 259 | print("Ignoring response: {}: {}".format(res.req, res.result)) 260 | 261 | def send_async(self, req, *args, **kwargs): 262 | print("send async: {}".format(req)) 263 | self.pynet_handler.put(PynetMessage(req, *args, **kwargs)) 264 | 265 | if __name__ == "__main__": 266 | pass 267 | -------------------------------------------------------------------------------- /examples/ksp-pynet/ksppynet/flight_plan_node.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import krpc 3 | import time 4 | import math 5 | 6 | def total_area(orbit): 7 | a = orbit.semi_major_axis 8 | b = orbit.semi_minor_axis 9 | return math.pi * a * b 10 | 11 | def area_since_periapsis(orbit, E): 12 | a = orbit.semi_major_axis 13 | b = orbit.semi_minor_axis 14 | return 0.5 * a * b * E 15 | 16 | def mean_anomaly_from_true_anomaly(orbit, theta): 17 | e = orbit.eccentricity 18 | adj = e + math.cos(theta) 19 | hyp = 1 + e*math.cos(theta) 20 | return math.acos(adj / hyp) 21 | 22 | def area_between_mean_anomalies(orbit, E0, E1): 23 | A0 = area_since_periapsis(orbit, E0) 24 | A1 = area_since_periapsis(orbit, E1) 25 | return A1 - A0 26 | 27 | def time_to_ascending_node(orbit): 28 | A0 = total_area(orbit) 29 | 30 | E = orbit.mean_anomaly 31 | Ap = area_since_periapsis(orbit, E) 32 | 33 | theta = orbit.argument_of_periapsis - math.pi 34 | En = mean_anomaly_from_true_anomaly(orbit, theta) 35 | En += math.pi 36 | An = area_since_periapsis(orbit, En) 37 | 38 | frac = (An-Ap)/A0 39 | return orbit.period * frac 40 | 41 | def time_to_descending_node(orbit): 42 | A0 = total_area(orbit) 43 | 44 | E = orbit.mean_anomaly 45 | Ap = area_since_periapsis(orbit, E) 46 | 47 | theta = orbit.argument_of_periapsis - math.pi 48 | En = mean_anomaly_from_true_anomaly(orbit, theta) 49 | An = area_since_periapsis(orbit, En) 50 | 51 | frac = (An-Ap)/A0 52 | return orbit.period * frac 53 | 54 | 55 | class ManeuverNode(object): 56 | def __init__(self, conn, vessel, ut): 57 | self.node = None 58 | self.conn = conn 59 | self.vessel = vessel 60 | self.ut = ut 61 | 62 | def circularize(self, at_apoapsis=True): 63 | conn = self.conn 64 | vessel = self.vessel 65 | orbit = vessel.orbit 66 | mu = orbit.body.gravitational_parameter 67 | if at_apoapsis: 68 | r = orbit.apoapsis 69 | else: 70 | r = orbit.periapsis 71 | a1 = orbit.semi_major_axis 72 | a2 = r 73 | v1 = math.sqrt(mu*((2./r)-(1./a1))) 74 | v2 = math.sqrt(mu*((2./r)-(1./a2))) 75 | delta_v = v2 - v1 76 | ut = conn.space_center.ut 77 | if at_apoapsis: 78 | ut += orbit.time_to_apoapsis 79 | else: 80 | ut += orbit.time_to_periapsis 81 | self.node = vessel.control.add_node(ut, prograde=delta_v) 82 | 83 | def change_apoapsis(self, new_apoapsis_altitude): 84 | conn = self.conn 85 | vessel = self.vessel 86 | orbit = vessel.orbit 87 | new_apoapsis = orbit.periapsis - orbit.periapsis_altitude + new_apoapsis_altitude 88 | mu = orbit.body.gravitational_parameter 89 | ut = conn.space_center.ut + orbit.time_to_periapsis 90 | r1 = orbit.periapsis 91 | a1 = orbit.semi_major_axis 92 | v1 = math.sqrt(mu*((2./r1)-(1./a1))) 93 | r2 = r1 94 | a2 = (r1 + new_apoapsis) / 2 95 | v2 = math.sqrt(mu*((2./r2)-(1./a2))) 96 | delta_v = v2 - v1 97 | print("new: {}, r1: {}, a1: {}, r2: {}, a2: {}, dV: {}".format( 98 | new_apoapsis, r1, a1, r2 ,a2, delta_v)) 99 | self.node = vessel.control.add_node(ut, prograde=delta_v) 100 | 101 | def change_periapsis(self, new_periapsis_altitude): 102 | conn = self.conn 103 | vessel = self.vessel 104 | orbit = vessel.orbit 105 | new_periapsis = orbit.apoapsis - orbit.apoapsis_altitude + new_periapsis_altitude 106 | mu = orbit.body.gravitational_parameter 107 | ut = conn.space_center.ut + orbit.time_to_apoapsis 108 | r1 = orbit.apoapsis 109 | a1 = orbit.semi_major_axis 110 | v1 = math.sqrt(mu*((2./r1)-(1./a1))) 111 | r2 = r1 112 | a2 = (r1 + new_periapsis) / 2 113 | v2 = math.sqrt(mu*((2./r2)-(1./a2))) 114 | delta_v = v2 - v1 115 | print("new: {}, r1: {}, a1: {}, r2: {}, a2: {}, dV: {}".format( 116 | new_periapsis, r1, a1, r2 ,a2, delta_v)) 117 | self.node = vessel.control.add_node(ut, prograde=delta_v) 118 | 119 | # def change_apoapsis(self, new_apoapsis): 120 | # conn = self.conn 121 | # vessel = self.vessel 122 | # orbit = vessel.orbit 123 | # mu = orbit.body.gravitational_parameter 124 | # ut = conn.space_center.ut + orbit.time_to_periapsis 125 | # r1 = orbit.periapsis 126 | # a1 = orbit.semi_major_axis 127 | # v1 = math.sqrt(mu*((2./r1)-(1./a1))) 128 | # r2 = r1 129 | # a2 = (r1 + new_apoapsis) / 2 130 | # v2 = math.sqrt(mu*((2./r2)-(1./a2))) 131 | # delta_v = v2 - v1 132 | # self.node = vessel.control.add_node(ut, prograde=delta_v) 133 | 134 | # def change_periapsis(self, new_periapsis): 135 | # conn = self.conn 136 | # vessel = self.vessel 137 | # orbit = vessel.orbit 138 | # mu = orbit.body.gravitational_parameter 139 | # ut = conn.space_center.ut + orbit.time_to_apoapsis 140 | # r1 = orbit.apoapsis 141 | # a1 = orbit.semi_major_axis 142 | # v1 = math.sqrt(mu*((2./r1)-(1./a1))) 143 | # r2 = r1 144 | # a2 = (r1 + new_periapsis) / 2 145 | # v2 = math.sqrt(mu*((2./r2)-(1./a2))) 146 | # delta_v = v2 - v1 147 | # self.node = vessel.control.add_node(ut, prograde=delta_v) 148 | 149 | def change_sma(self, sma, ut): 150 | conn = self.conn 151 | vessel = self.vessel 152 | orbit = vessel.orbit 153 | mu = orbit.body.gravitational_parameter 154 | r1 = orbit.radius 155 | a1 = orbit.semi_major_axis 156 | v1 = math.sqrt(mu*((2./r1)-(1./a1))) 157 | r2 = r1 158 | a2 = sma 159 | v2 = math.sqrt(mu*((2./r2)-(1./a2))) 160 | delta_v = v2 - v1 161 | self.node = vessel.control.add_node(ut, prograde=delta_v) 162 | 163 | def hohmann_transfer(self, target): 164 | conn = self.conn 165 | vessel = self.vessel 166 | orbit = vessel.orbit 167 | mu = orbit.body.gravitational_parameter 168 | r1 = orbit.radius 169 | r2 = target.orbit.radius 170 | 171 | transfer_time = math.pi * math.sqrt(((r1+r2)**3)/(8*mu)) 172 | transfer_angle = math.pi * (1 - (1/(2*math.sqrt(2))) * math.sqrt(((r1/r2)+1)**3)) 173 | 174 | Mv = orbit.mean_anomaly 175 | Mt = target.orbit.mean_anomaly 176 | av = orbit.semi_major_axis 177 | at = target.orbit.semi_major_axis 178 | nv = math.sqrt(mu/(av**3)) 179 | nt = math.sqrt(mu/(at**3)) 180 | 181 | Mt += 2*math.pi 182 | 183 | time_until_transfer = (transfer_angle - Mt + Mv) / (nt - nv) 184 | 185 | print(Mt, Mv, transfer_angle, transfer_angle - Mt + Mv, nt - nv, time_until_transfer) 186 | 187 | ut = conn.space_center.ut + time_until_transfer 188 | 189 | a1 = orbit.semi_major_axis 190 | v1 = math.sqrt(mu*((2./r1)-(1./a1))) 191 | a2 = (target.orbit.radius + r1) / 2 192 | v2 = math.sqrt(mu*((2./r1)-(1./a2))) 193 | delta_v = v2 - v1 194 | 195 | self.node = vessel.control.add_node(ut, prograde=delta_v) 196 | 197 | 198 | def change_inclination(self, new_inclination): 199 | conn = self.conn 200 | vessel = self.vessel 201 | orbit = vessel.orbit 202 | i = new_inclination - orbit.inclination 203 | v = orbit.speed 204 | normal = v*math.sin(i) 205 | prograde = v*math.cos(i) - v 206 | ut = conn.space_center.ut + time_to_ascending_node(orbit) 207 | self.node = vessel.control.add_node(ut, normal=normal, prograde=prograde) 208 | 209 | @asyncio.coroutine 210 | def execute(self, lead_time=15): 211 | conn = self.conn 212 | vessel = self.vessel 213 | node = self.node 214 | 215 | if not node: 216 | return True 217 | 218 | self.lead_time = lead_time 219 | self.remaining_burn = conn.add_stream(node.remaining_burn_vector, node.reference_frame) 220 | self.node_ut = node.ut 221 | 222 | # Calculate burn time using rocket equation 223 | F = vessel.available_thrust 224 | Isp = vessel.specific_impulse * 9.82 225 | m0 = vessel.mass 226 | m1 = m0 / math.exp(node.delta_v/Isp) 227 | flow_rate = F / Isp 228 | self.burn_time = (m0 - m1) / flow_rate 229 | 230 | # Orientate ship 231 | ap = vessel.auto_pilot 232 | ap.reference_frame = node.reference_frame 233 | ap.target_direction = (0,1,0) 234 | ap.engage() 235 | while True: 236 | yield 237 | burn_ut = self.node_ut - (self.burn_time/2.) 238 | #TODO: check vessel is pointing in the correct direction before warping 239 | # and if the error is large, drop out of warp and reorient the vessel 240 | if self.ut() < burn_ut - self.lead_time: 241 | self.conn.space_center.warp_to(burn_ut - self.lead_time) 242 | 243 | if self.ut() < burn_ut: 244 | continue 245 | 246 | # Burn time remaining 247 | try: 248 | F = self.vessel.available_thrust 249 | Isp = self.vessel.specific_impulse * 9.82 250 | m0 = self.vessel.mass 251 | m1 = m0 / math.exp(self.remaining_burn()[1]/Isp) 252 | flow_rate = F / Isp 253 | remaining_burn_time = (m0 - m1) / flow_rate 254 | except ZeroDivisionError: 255 | continue 256 | 257 | if remaining_burn_time > 2: 258 | # Burn at full throttle 259 | self.vessel.control.throttle = 1 260 | continue 261 | elif self.remaining_burn()[1] > 0: 262 | # Burn at a throttle setting that maintains a 263 | # remaining burn time of t seconds 264 | t = 2 265 | F = ((m0 - m1) / t) * Isp 266 | throttle = F / self.vessel.available_thrust # XXX Divide by zero! 267 | self.vessel.control.throttle = max(0.005, throttle) 268 | continue 269 | else: 270 | # Burn complete 271 | self.vessel.control.throttle = 0 272 | self.node.remove() 273 | self.node = None 274 | return 275 | 276 | -------------------------------------------------------------------------------- /examples/ksp-pynet/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | from kivy.app import App 4 | from kivy.uix.boxlayout import BoxLayout 5 | from kivy.uix.gridlayout import GridLayout 6 | from kivy.uix.relativelayout import RelativeLayout 7 | from kivy.uix.stacklayout import StackLayout 8 | from kivy.uix.scrollview import ScrollView 9 | from kivy.properties import ObjectProperty, StringProperty, NumericProperty 10 | from kivy.uix.button import Button 11 | from kivy.uix.dropdown import DropDown 12 | from kivy.uix.label import Label 13 | from kivy.uix.textinput import TextInput 14 | from kivy.uix.widget import Widget 15 | from kivy.clock import Clock 16 | from kivy.graphics import Ellipse, Color 17 | import re 18 | import math 19 | import ksppynet 20 | import traceback 21 | from functools import partial 22 | 23 | 24 | 25 | class MenuActive(ScrollView): 26 | pass 27 | 28 | class DisplayManeuver(GridLayout): 29 | def __init__(self, **kwargs): 30 | super(DisplayManeuver, self).__init__(**kwargs) 31 | 32 | self.maneuvers = {"apoapsis" : "120000", 33 | "periapsis": "100000"} 34 | 35 | 36 | self.dropdown = DropDown() 37 | self.input = FloatInput(size=(200, 30), 38 | multiline=False, 39 | size_hint=(None, None)) 40 | 41 | for m in self.maneuvers.keys(): 42 | btn = Button(text=m, size_hint_y=None, height=30) 43 | 44 | # Pass the text of the button as the data of the selection. 45 | btn.bind(on_release=lambda btn: self.dropdown.select(btn.text)) 46 | self.dropdown.add_widget(btn) 47 | 48 | self.mainbutton = Button(text='Maneuver', 49 | size=(160, 50), 50 | size_hint=(None, None)) 51 | 52 | self.mainbutton.bind(on_release=self.dropdown.open) 53 | 54 | def dropdown_action(instance, m): 55 | self.input.text = self.maneuvers[m] 56 | self.mainbutton.text = m 57 | 58 | self.dropdown.bind(on_select=dropdown_action) 59 | 60 | self.go_button = Button(text='Go', 61 | size=(160, 50), 62 | size_hint=(None, None)) 63 | self.go_button.bind(on_press=self.go) 64 | 65 | self.add_widget(self.mainbutton) 66 | self.add_widget(self.input) 67 | self.add_widget(self.go_button) 68 | 69 | def go(self, *args): 70 | kwargs = {self.mainbutton.text : float(self.input.text)} 71 | app = App.get_running_app() 72 | app.pynet.send_async("maneuver", **kwargs) 73 | 74 | class Viewer(Widget): 75 | body_name = StringProperty() 76 | 77 | def __init__(self, **kwargs): 78 | super(Viewer, self).__init__(**kwargs) 79 | with self.canvas: 80 | Color(0.0, 0.1, 0.9, 0.8) 81 | self.orbit = Ellipse() 82 | Color(0.1, 0.1, 0.2, 1) 83 | self.orbit_inner = Ellipse() 84 | Color(0.2, 0.3, 0.1, 1) 85 | self.body = Ellipse() 86 | self.bind(pos=self.update, size=self.update) 87 | self.real_apoapsis = 1000000 88 | self.real_periapsis = 400000 89 | self.real_radius = 50000 90 | self.rescale() 91 | 92 | def rescale(self): 93 | # Put the body in the middle, with the apoapsis near the edge of the 94 | # screen. 95 | try: 96 | factor1 = self.width / (float(max(self.real_apoapsis, self.real_radius)) * 2) 97 | factor2 = self.height / (float(max(math.sqrt(self.real_apoapsis * 98 | self.real_periapsis), 99 | self.real_radius)) * 2) 100 | factor = min(factor1, factor2) 101 | factor *= 0.9 102 | self.radius = self.real_radius * factor 103 | self.apoapsis = self.real_apoapsis * factor 104 | self.periapsis = self.real_periapsis * factor 105 | except: 106 | # if we cant' rescale, don't 107 | print(traceback.format_exc()) 108 | 109 | 110 | def update(self, *args): 111 | self.rescale() 112 | self.body.pos = self.body_pos() 113 | self.body.size = self.body_size() 114 | self.orbit.pos = self.orbit_pos() 115 | self.orbit.size = self.orbit_size() 116 | self.orbit_inner.pos = (self.orbit.pos[0] + 2, self.orbit.pos[1] + 2) 117 | self.orbit_inner.size = (self.orbit.size[0] - 4, self.orbit.size[1] - 4) 118 | 119 | @property 120 | def center(self): 121 | return(self.width/2.0, self.height/2.0) 122 | 123 | @property 124 | def _x(self): 125 | return math.sqrt((self.apoapsis * self.periapsis)) 126 | 127 | def body_pos(self): 128 | return (self.center[0] - self.radius, self.center[1] - self.radius) 129 | 130 | def body_size(self): 131 | return (2*self.radius, 2*self.radius) 132 | 133 | def orbit_pos(self): 134 | return (self.center[0] - self.periapsis, self.center[1] - self._x) 135 | 136 | def orbit_size(self): 137 | return (self.apoapsis + self.periapsis, 2 * self._x) 138 | 139 | def update_params(self, msgid, odict): 140 | self.real_apoapsis = odict["real_apoapsis"] 141 | self.real_periapsis = odict["real_periapsis"] 142 | self.real_radius = odict["body_radius"] 143 | self.body_name = odict["body_name"] 144 | self.update() 145 | 146 | def register(self, app, on): 147 | app.pynet.register("orbital_notify", self.update_params) 148 | app.pynet.send_async("orbital", on) 149 | 150 | 151 | class Menu(BoxLayout): 152 | connection = ObjectProperty() 153 | dynamic = ObjectProperty() 154 | alerts = ObjectProperty() 155 | 156 | def __init__(self, **kwargs): 157 | super(Menu, self).__init__(**kwargs) 158 | self.active_widget = None 159 | self.message_buffer = [{}] 160 | Clock.schedule_interval(self.update_msg, 1.0) 161 | 162 | def active(self, enable): 163 | if enable and not self.active_widget: 164 | self.active_widget = MenuActive() 165 | self.connection.background_color = (0.5,1,0.5,0.8) 166 | self.dynamic.add_widget(self.active_widget) 167 | elif not enable and self.active_widget: 168 | self.connection.background_color = (1,0.1,0.1,1) 169 | self.dynamic.remove_widget(self.active_widget) 170 | self.active_widget = None 171 | 172 | def update_msg(self, dt): 173 | try: 174 | self.message_buffer.pop(0) 175 | out = "\n".join([m for m in self.message_buffer if m]).strip() 176 | self.alerts.text = out 177 | except: 178 | self.alerts.text = "" 179 | 180 | def add_msg(self, msg, duration=5): 181 | extra = duration - len(self.message_buffer) + 1 182 | if extra > 0: 183 | self.message_buffer = self.message_buffer + [""] * extra 184 | self.message_buffer[duration] = "\n".join([self.message_buffer[duration], msg]).strip() 185 | 186 | class FloatInput(TextInput): 187 | pat = re.compile('[^0-9]') 188 | def insert_text(self, substring, from_undo=False): 189 | pat = self.pat 190 | if '.' in self.text: 191 | s = re.sub(pat, '', substring) 192 | else: 193 | s = '.'.join([re.sub(pat, '', s) for s in substring.split('.', 1)]) 194 | return super(FloatInput, self).insert_text(s, from_undo=from_undo) 195 | 196 | class DisplayConnection(GridLayout): 197 | connected = ObjectProperty() 198 | connected_state = ObjectProperty() 199 | def go(self, app): 200 | info = { k: self.ids[k] for k in ("ip", "port")} 201 | if self.connected_state == str(True): 202 | app.disconnect() 203 | else: 204 | app.connect(**info) 205 | 206 | def set_connected(self, on): 207 | self.connected_state = str(on) 208 | if on: 209 | self.connected.text = "Disconnect" 210 | self.connected.background_color = (1,0.1,0.1,1) 211 | else: 212 | self.connected.text = "Connect" 213 | self.connected.background_color = (0.5,1,0.5,0.8) 214 | 215 | class DisplayFlightPlan(GridLayout): 216 | def go(self, app): 217 | info = { k: float(self.ids[k].state) for k in ("heading", "target_altitude", 218 | "apoapsis", "periapsis")} 219 | app.pynet.send_async("plan", **info) 220 | 221 | class DisplayState(BoxLayout): 222 | pass 223 | 224 | class DisplayDiagnostics(BoxLayout): 225 | pass 226 | 227 | class Display(RelativeLayout): 228 | sm = ObjectProperty() 229 | 230 | class Console(BoxLayout): 231 | display = ObjectProperty() 232 | menu = ObjectProperty() 233 | 234 | 235 | class KpConsoleApp(App): 236 | def connection_handler(self, msgid, msg_dict): 237 | if msgid == "connect" and "error" not in msg_dict: 238 | self.console.display.dconn.set_connected(True) 239 | self.console.menu.active(True) 240 | else: 241 | self.console.display.dconn.set_connected(False) 242 | self.console.menu.active(False) 243 | print("{}: {}".format(msgid, msg_dict)) 244 | 245 | @property 246 | def screen(self): 247 | return self.console.display.sm 248 | 249 | def default_recv_msg_handler(self, msgid, msg_dict): 250 | if msgid == "message" and "msg" in msg_dict: 251 | self.console.menu.add_msg(**msg_dict) 252 | else: 253 | print("Unhandled response: {}: {}".format(msgid, msg_dict)) 254 | 255 | def pynet_response_handler(self, dt): 256 | self.pynet.recv_async() 257 | 258 | def disconnect(self): 259 | self.pynet.disconnect() 260 | 261 | def connect(self, **kwargs): 262 | self.pynet.connect(self.connection_handler, 263 | default_callback=self.default_recv_msg_handler, 264 | **kwargs) 265 | self.console.menu.add_msg("Connecting...") 266 | Clock.schedule_interval(self.pynet_response_handler, 1.0 / 60.0) 267 | 268 | def build(self): 269 | self.pynet = ksppynet.Pynet() 270 | self.console = Console() 271 | return self.console 272 | 273 | if __name__ == '__main__': 274 | if len(sys.argv) > 1: 275 | #Turn on Test/debug mode. 276 | ksppynet.ksppynet.testing_ui_on = True 277 | KpConsoleApp().run() 278 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/rendezvous.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | ### Automated Rendezvous Library and Example 3 | ###################################################################### 4 | ### Like all of the scripts in my folder here, this file contains 5 | ### functions you might want to include into your own scripts for 6 | ### actual use and a demo in the 'main' function that you can just 7 | ### run to see how it works. 8 | ### 9 | ### This file shows how to complete an orbital rendezvous with 10 | ### a target vessel. 11 | ### 12 | ### Important note - contains future functions of KRPC! Won't work 13 | ### with release versions until 4.0 comes out! 14 | ###################################################################### 15 | 16 | import krpc 17 | import time 18 | import math 19 | 20 | from node_executor import execute_next_node 21 | 22 | ############################################################################### 23 | ## Main Function 24 | ## Demonstrates Rendezvous with selected target vessel 25 | ## This function is not called when file is imported into YOUR scripts. 26 | ############################################################################### 27 | def main(): 28 | conn=krpc.connect() 29 | sc = conn.space_center 30 | v = conn.space_center.active_vessel 31 | t = conn.space_center.target_vessel 32 | 33 | match_planes(conn) 34 | hohmann(conn) 35 | circularize_at_intercept(conn) 36 | get_closer(conn) 37 | print ("Rendezvous Complete.") 38 | 39 | ############################################################################### 40 | ## Automated Rendezvous Functions 41 | ## High Level Functions that perform each phase of a rendezvous 42 | ############################################################################### 43 | 44 | def match_planes(conn): 45 | print("Computing Plane Change Maneuver if Necessary...") 46 | sc = conn.space_center 47 | v = sc.active_vessel 48 | t = sc.target_vessel 49 | ascending = False 50 | time = sc.ut 51 | if v.orbit.relative_inclination(t) < 0.004363323129985824: #.25 degree 52 | print("Planes within .25 degree. Continuing with program...") 53 | return 54 | 55 | #figure out if AN or DN is soonest. Since Script assumes circular orbits 56 | #it doesn't worry about which is highest (cheapest burn). 57 | ut_an = v.orbit.ut_at_true_anomaly(v.orbit.true_anomaly_an(t)) 58 | ut_dn = v.orbit.ut_at_true_anomaly(v.orbit.true_anomaly_dn(t)) 59 | if ut_an < ut_dn: 60 | ascending = True 61 | time = ut_an 62 | else: 63 | ascending = False 64 | time = ut_dn 65 | 66 | #calculate plane change burn 67 | sp = v.orbit.orbital_speed_at(time) 68 | inc = v.orbit.relative_inclination(t) 69 | normal = sp * math.sin(inc) 70 | prograde = sp * math.cos(inc) - sp 71 | if ascending: 72 | normal *= -1 #antinormal at ascending node 73 | 74 | node = v.control.add_node(time, normal=normal, prograde=prograde) 75 | print("Executing Plane Change of {} m/s...".format(node.delta_v)) 76 | execute_next_node(conn) 77 | 78 | 79 | def hohmann(conn): 80 | print ("Plotting Hohmann Transfer Maneuver...") 81 | sc = conn.space_center 82 | v = sc.active_vessel 83 | t = sc.target_vessel 84 | time = sc.ut 85 | phase_angle = get_phase_angle(v, t) 86 | transfer_time = time_transfer(v, t, time, phase_angle) 87 | node = hohmann_transfer(v, t, transfer_time) 88 | print ("Executing transfer injection burn {} m/s...".format(node.delta_v)) 89 | execute_next_node(conn) 90 | 91 | def circularize_at_intercept(conn): 92 | print ("Plotting dV2 Burn to Circularize...") 93 | v = conn.space_center.active_vessel 94 | time = conn.space_center.ut 95 | node = circularize_at_apoapsis(v, time) 96 | print("Executing circularization burn of {} m/s...".format(node.delta_v)) 97 | execute_next_node(conn) 98 | 99 | def get_closer(conn): 100 | sc = conn.space_center 101 | v = sc.active_vessel 102 | t = sc.target_vessel 103 | ap = v.auto_pilot 104 | rf = v.orbit.body.non_rotating_reference_frame 105 | ap.reference_frame=rf 106 | 107 | matchv(sc, v, t) 108 | while dist(v, t) > 200: 109 | close_dist(sc, v, t) 110 | 111 | matchv(sc, v, t) 112 | 113 | 114 | 115 | ############################################################################### 116 | ## Major Calculation Functions 117 | ## Mid Level Functions that perform the calculations required 118 | ############################################################################### 119 | 120 | def get_phase_angle(vessel, target): 121 | ''' 122 | returns the relative phase angle for a hohmann transfer 123 | ''' 124 | vo=vessel.orbit 125 | to=target.orbit 126 | h=(vo.semi_major_axis+to.semi_major_axis)/2 # SMA of transfer orbit 127 | #calculate the percentage of the target orbit that goes by during the half period of transfer orbit 128 | p = 1/(2*math.sqrt(math.pow(to.semi_major_axis,3)/math.pow(h,3))) 129 | # convert that to an angle in radians 130 | a = (2 * math.pi) - ((2* math.pi) *p) 131 | print("Transfer Phase Angle is {}.".format(a)) 132 | return a 133 | 134 | def orbital_progress(vessel, ut): 135 | ''' 136 | returns the orbital progress in radians, referenced to the planet's origin 137 | of longitude. 138 | ''' 139 | lan = vessel.orbit.longitude_of_ascending_node 140 | arg_p = vessel.orbit.argument_of_periapsis 141 | ma_ut = vessel.orbit.mean_anomaly_at_ut(ut) 142 | return clamp_2pi(lan + arg_p + ma_ut) 143 | 144 | def time_transfer(vessel, target, ut, phase_angle): 145 | ''' 146 | Performs an iterative search for the next time vessel and target 147 | have the given relative phase_angle after ut 148 | ''' 149 | print("Doing Coarse Search for Transfer Time...") 150 | #coarse unbound search 151 | while True: 152 | v_pos = orbital_progress(vessel, ut) 153 | t_pos = orbital_progress(target, ut) 154 | angle_error = math.fabs(t_pos - (v_pos - math.pi) - phase_angle) 155 | if angle_error < .01: 156 | break 157 | ut += 10 158 | ut -= 10 159 | #fine unbound search 160 | print("Doing Fine Search for Transfer Time...") 161 | while True: 162 | v_pos = orbital_progress(vessel, ut) 163 | t_pos = orbital_progress(target, ut) 164 | angle_error = math.fabs(t_pos - (v_pos - math.pi) - phase_angle) 165 | if angle_error < .001: 166 | break 167 | ut += 1 168 | return ut 169 | 170 | def hohmann_transfer(vessel, target, time): 171 | ''' 172 | Create a maneuver node for a hohmann transfer from vessel orbit to target 173 | orbit at the given time 174 | ''' 175 | body = vessel.orbit.body 176 | GM = body.gravitational_parameter 177 | r1 = vessel.orbit.radius_at(time) 178 | SMA_i = vessel.orbit.semi_major_axis 179 | SMA_t = (vessel.orbit.apoapsis + target.orbit.apoapsis) / 2 180 | v1 = math.sqrt(GM * ((2/r1) - (1 / SMA_i))) 181 | v2 = math.sqrt(GM * ((2/r1) - (1 / (SMA_t)))) 182 | dv = v2 - v1 183 | return vessel.control.add_node(time, prograde = dv) 184 | 185 | def circularize_at_apoapsis(vessel, ut): 186 | ''' 187 | Create a maneuver node to circularize orbit at given time 188 | ''' 189 | body = vessel.orbit.body 190 | GM = body.gravitational_parameter 191 | apo = vessel.orbit.apoapsis 192 | SMA = vessel.orbit.semi_major_axis 193 | v1 = math.sqrt(GM * ((2 / apo) - (1 / SMA))) 194 | v2 = math.sqrt(GM * ((2 / apo) - (1 / (apo)))) 195 | dv = v2 - v1 196 | time = vessel.orbit.time_to_apoapsis + ut 197 | return vessel.control.add_node(time, prograde = dv) 198 | 199 | def matchv(sc, v, t): 200 | ''' 201 | function to match active vessel's velocity to target's at the 202 | point of closest approach 203 | ''' 204 | print ("Matching Velocites...") 205 | 206 | # Calculate the length and start of burn 207 | m = v.mass 208 | isp = v.specific_impulse 209 | dv = speed(v, t) 210 | F = v.available_thrust 211 | G = 9.81 212 | burn_time = (m - (m / math.exp(dv / (isp * G)))) / (F / (isp * G)) 213 | 214 | ## Orient vessel to negative target relative velocity 215 | ap = v.auto_pilot 216 | ap.engage() 217 | ap.target_direction = target_vminus(v,t) 218 | ap.wait() 219 | 220 | #wait for the time to burn 221 | burn_start = v.orbit.time_of_closest_approach(t) - (burn_time/1.9) 222 | sc.warp_to(burn_start - 10) 223 | while sc.ut < burn_start: 224 | ap.target_direction = target_vminus(v,t) 225 | time.sleep(.5) 226 | #burn 227 | while speed(v, t) > .1: 228 | ap.target_direction = target_vminus(v,t) 229 | v.control.throttle = speed(v, t) / 20.0 230 | 231 | #restore user control 232 | v.control.throttle = 0.0 233 | ap.disengage() 234 | 235 | 236 | 237 | 238 | def close_dist(sc, v, t): 239 | ''' 240 | Function to close distance between active and target vessels. 241 | Sets approach speed to 1/200 of separation at time of burn. 242 | ''' 243 | print ("Closing Distance...") 244 | 245 | # orient vessel to target 246 | ap = v.auto_pilot 247 | ap.engage() 248 | time.sleep(.1) 249 | ap.target_direction = target(v, t) 250 | time.sleep(.1) 251 | ap.wait() 252 | 253 | #calculate and burn 254 | targetspeed = dist(v,t) / 200.0 255 | while targetspeed - speed(v, t) > .1: 256 | ap.target_direction = target(v,t) 257 | v.control.throttle = (targetspeed - speed(v, t)) / 20.0 258 | 259 | #restore user control 260 | v.control.throttle = 0.0 261 | ap.disengage() 262 | 263 | 264 | 265 | 266 | 267 | ############################################################################### 268 | ## Helper Functions 269 | ## Low Level Functions - mostly designed for internal use 270 | ############################################################################### 271 | 272 | def clamp_2pi(x): 273 | ''' 274 | clamp radians to a single revolution 275 | ''' 276 | while x > (2 * math.pi): 277 | x -= (2 * math.pi) 278 | return x 279 | 280 | def v3minus(v,t): 281 | ''' 282 | vector subtraction 283 | ''' 284 | a = v[0]-t[0] 285 | b = v[1]-t[1] 286 | c = v[2]-t[2] 287 | return (a,b,c) 288 | 289 | 290 | def target(v,t): 291 | ''' 292 | returns vector to point at target 293 | in vessel.orbit.body.non_rotating_reference_frame 294 | ''' 295 | rf = v.orbit.body.non_rotating_reference_frame 296 | return v3minus(t.position(rf), v.position(rf)) 297 | 298 | def anti_target(v,t): 299 | ''' 300 | returns vector to point away from target 301 | in vessel.orbit.body.non_rotating_reference_frame 302 | ''' 303 | rf = v.orbit.body.non_rotating_reference_frame 304 | return v3minus(v.position(rf), t.position(rf)) 305 | 306 | def target_vplus(v,t): 307 | ''' 308 | returns vector to point at + target velocity 309 | in vessel.orbit.body.non_rotating_reference_frame 310 | ''' 311 | rf = v.orbit.body.non_rotating_reference_frame 312 | return v3minus(v.velocity(rf), t.velocity(rf)) 313 | 314 | def target_vminus(v,t): 315 | ''' 316 | returns vector to point at - target velocity 317 | in vessel.orbit.body.non_rotating_reference_frame 318 | ''' 319 | rf = v.orbit.body.non_rotating_reference_frame 320 | return v3minus(t.velocity(rf), v.velocity(rf)) 321 | 322 | def dist(v,t): 323 | ''' 324 | returns distance (magnitude) between two 325 | positions 326 | ''' 327 | rf = v.orbit.body.non_rotating_reference_frame 328 | vec = v3minus(v.position(rf), t.position(rf)) 329 | a = vec[0] * vec[0] 330 | b = vec[1] * vec[1] 331 | c = vec[2] * vec[2] 332 | return math.sqrt(a+b+c) 333 | 334 | def speed(v,t): 335 | ''' 336 | returns speed (magnitude) between two 337 | velocities 338 | ''' 339 | rf = v.orbit.body.non_rotating_reference_frame 340 | vec = v3minus(v.velocity(rf), t.velocity(rf)) 341 | a = vec[0] * vec[0] 342 | b = vec[1] * vec[1] 343 | c = vec[2] * vec[2] 344 | return math.sqrt(a+b+c) 345 | 346 | 347 | # ---------------------------------------------------------------------------- 348 | # Activate main function, assuming we are executing THIS file explicitly. 349 | # ---------------------------------------------------------------------------- 350 | if __name__ == "__main__" : 351 | main() 352 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/Simple_Launch_Script.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | ### Fairly Robust Launch Script 3 | ###################################################################### 4 | ### Kerbart and I are working on a more versatile and pythonic 5 | ### launch script that uses the same logic as this one, but I 6 | ### thought that it was still worth posting this simpler version 7 | ### for perusal (or use!) 8 | ###################################################################### 9 | 10 | import krpc 11 | import time 12 | import math 13 | 14 | from pid import PID 15 | from node_executor import execute_next_node 16 | 17 | # ---------------------------------------------------------------------------- 18 | # Script parameters 19 | # ---------------------------------------------------------------------------- 20 | 21 | REFRESH_FREQ = 2 # refresh rate in hz 22 | TELEM_DELAY = 5 #number of seconds between telemetry updates 23 | ALL_FUELS = ('LiquidFuel', 'SolidFuel') 24 | MAX_PHYSICS_WARP = 3 # valid values are 0 (none) through 3 (4x) 25 | next_telem_time=time.time() 26 | 27 | class MissionParameters(object): 28 | ''' 29 | All mission parameters are stored in a single object to easily 30 | pass around 31 | ''' 32 | def __init__(self, 33 | max_auto_stage = 0, 34 | orbit_alt = 100000, 35 | grav_turn_finish = 55000, 36 | inclination = 0, 37 | force_roll = True, 38 | roll = 90, 39 | deploy_solar = True, 40 | max_q = 20000): 41 | self.max_auto_stage = max_auto_stage 42 | self.orbit_alt = orbit_alt 43 | self.grav_turn_finish = grav_turn_finish 44 | self.inclination = inclination 45 | self.force_roll = force_roll 46 | self.roll = roll 47 | self.deploy_solar = deploy_solar 48 | self.max_q = max_q 49 | 50 | class Telemetry(object): 51 | def __init__(self, vessel, flight): 52 | self.apoapsis = vessel.orbit.apoapsis_altitude 53 | self.periapsis = vessel.orbit.periapsis_altitude 54 | self.time_to_apo = vessel.orbit.time_to_apoapsis 55 | self.time_to_peri = vessel.orbit.time_to_periapsis 56 | self.velocity = vessel.orbit.speed 57 | self.inclination = math.radians(vessel.orbit.inclination) 58 | self.altitude = flight.mean_altitude 59 | self.vertical_speed = flight.vertical_speed 60 | self.lat = flight.latitude 61 | self.lon = flight.longitude 62 | self.q = flight.dynamic_pressure 63 | self.g = flight.g_force 64 | 65 | 66 | 67 | # ---------------------------------------------------------------------------- 68 | # Main loop 69 | # ---------------------------------------------------------------------------- 70 | 71 | def main(): 72 | ''' 73 | main function is run when you just execute this file, but NOT when you 74 | import it into another file - thus you can choose to call ascent later 75 | to go to space, or just use the other functions in this file. 76 | ''' 77 | 78 | ## Setup KRPC and create a launch_params object with the default settings 79 | conn = krpc.connect(name='Launch') 80 | launch_params = MissionParameters() 81 | ascent(conn,launch_params) 82 | 83 | def ascent(conn, launch_params): 84 | ''' 85 | Ascent Autopilot function. Goes to space, or dies trying. 86 | ''' 87 | #Setup KRPC and PIDs 88 | conn = krpc.connect(name='Launch') 89 | sc = conn.space_center 90 | v = sc.active_vessel 91 | telem=v.flight(v.orbit.body.reference_frame) 92 | thrust_controller = PID(P=.001, I=0.0001, D=0.01) 93 | thrust_controller.ClampI = launch_params.max_q 94 | thrust_controller.setpoint(launch_params.max_q) 95 | 96 | 97 | #Prepare for Launch 98 | v.auto_pilot.engage() 99 | v.auto_pilot.target_heading=inc_to_heading(launch_params.inclination) 100 | if launch_params.force_roll: 101 | v.auto_pilot.target_roll=launch_params.roll 102 | v.control.throttle=1.0 103 | 104 | #Gravity Turn Loop 105 | while apoapsis_way_low(v, launch_params.orbit_alt): 106 | gravturn(conn, launch_params) 107 | autostage(v , launch_params.max_auto_stage) 108 | limitq(conn, thrust_controller) 109 | telemetry(conn) 110 | time.sleep(1.0 / REFRESH_FREQ) 111 | v.control.throttle = 0.0 112 | 113 | # Fine Tune APA 114 | v.auto_pilot.disengage() 115 | v.auto_pilot.sas=True 116 | time.sleep(.1) 117 | v.auto_pilot.sas_mode = v.auto_pilot.sas_mode.prograde 118 | v.auto_pilot.wait() 119 | boostAPA(conn, launch_params) #fine tune APA 120 | 121 | # Coast Phase 122 | sc.physics_warp_factor = MAX_PHYSICS_WARP 123 | while still_in_atmosphere(conn): 124 | if apoapsis_little_low(v , launch_params.orbit_alt): 125 | sc.physics_warp_factor = 0 126 | boostAPA(conn, launch_params) 127 | sc.physics_warp_factor = MAX_PHYSICS_WARP 128 | telemetry(conn) 129 | time.sleep(1.0 / REFRESH_FREQ) 130 | 131 | # Circularization Burn 132 | sc.physics_warp_factor = 0 133 | planCirc(conn) 134 | telemetry(conn) 135 | execute_next_node(conn) 136 | 137 | # Finish Up 138 | if launch_params.deploy_solar: v.control.solar_panels=True 139 | telemetry(conn) 140 | v.auto_pilot.sas_mode= v.auto_pilot.sas_mode.prograde 141 | 142 | # ---------------------------------------------------------------------------- 143 | # staging logic 144 | # ---------------------------------------------------------------------------- 145 | 146 | def autostage(vessel, MAX_AUTO_STAGE): 147 | ''' 148 | activate next stage when there is no fuel left in the current stage 149 | ''' 150 | if out_of_stages(vessel, MAX_AUTO_STAGE): 151 | return 152 | res = get_resources(vessel) 153 | interstage = True # flag to check if this is a fuel-less stage 154 | for fueltype in ALL_FUELS: 155 | if out_of_fuel(res, fueltype): 156 | next_stage(vessel) 157 | return 158 | if res.has_resource(fueltype): 159 | interstage = False 160 | if interstage: 161 | next_stage(vessel) 162 | 163 | # ---------------------------------------------------------------------------- 164 | # guidance routines 165 | # ---------------------------------------------------------------------------- 166 | 167 | def gravturn(conn, launch_params): 168 | ''' 169 | Execute quadratic gravity turn - 170 | based on Robert Penner's easing equations (EaseOut) 171 | ''' 172 | vessel = conn.space_center.active_vessel 173 | flight = vessel.flight(vessel.orbit.body.non_rotating_reference_frame) 174 | progress=flight.mean_altitude/launch_params.grav_turn_finish 175 | vessel.auto_pilot.target_pitch= 90-(-90 * progress*(progress-2)) 176 | 177 | def boostAPA(conn, launch_params): 178 | ''' 179 | function to increase Apoapsis using low thrust on a 180 | tight loop with no delay for increased precision. 181 | ''' 182 | vessel = conn.space_center.active_vessel 183 | flight = vessel.flight(vessel.orbit.body.non_rotating_reference_frame) 184 | 185 | vessel.control.throttle=.2 186 | while apoapsis_little_low(vessel, launch_params.orbit_alt): 187 | autostage(vessel, launch_params.max_auto_stage) 188 | telemetry(conn) 189 | vessel.control.throttle=0 190 | 191 | def planCirc(conn): 192 | 193 | ''' 194 | Plan a Circularization at Apoapsis. 195 | V1 is velocity at apoapsis. 196 | V2 is the velocity at apoapsis of a circular orbit. 197 | Burn time uses Tsiolkovsky rocket equation. 198 | ''' 199 | vessel = conn.space_center.active_vessel 200 | ut = conn.space_center.ut 201 | grav_param = vessel.orbit.body.gravitational_parameter 202 | apo = vessel.orbit.apoapsis 203 | sma = vessel.orbit.semi_major_axis 204 | v1 = math.sqrt(grav_param * ((2.0 / apo) - (1.0 / sma))) 205 | v2 = math.sqrt(grav_param * ((2.0 / apo) - (1.0 / apo))) 206 | vessel.control.add_node(ut + vessel.orbit.time_to_apoapsis, 207 | prograde=(v2 - v1)) 208 | 209 | def inc_to_heading(inc): 210 | ''' 211 | Converts desired inclination to a compass heading the autopilot can track 212 | This only works for equatorial launches at the moment! 213 | inc: inclination in degrees 214 | returns: heading in degrees 215 | ''' 216 | if inc > 180 or inc < -180: 217 | return 90 #invalid entries get set to 0 inclination 218 | if inc >= 0: 219 | value = 90 - inc 220 | if inc < 0: 221 | value = -(inc - 90) 222 | if value < 0: 223 | value += 360 224 | return value 225 | 226 | def limitq(conn, controller): 227 | ''' 228 | limits vessel's throttle to stay under MAX_Q using PID controller 229 | ''' 230 | vessel = conn.space_center.active_vessel 231 | flight = vessel.flight(vessel.orbit.body.non_rotating_reference_frame) 232 | vessel.control.throttle= controller.update(flight.dynamic_pressure) 233 | 234 | 235 | # ---------------------------------------------------------------------------- 236 | # post telemetry 237 | # ---------------------------------------------------------------------------- 238 | 239 | def telemetry(conn): 240 | ''' 241 | Show telemetry data 242 | TODO: split between creating telemetry data (object? dict?) 243 | and displaying the data. This will make it easier to transition to a 244 | GUI later on. For this reason, no attempts to fit the lines has been 245 | made (yet) 246 | ''' 247 | vessel = conn.space_center.active_vessel 248 | flight = vessel.flight(vessel.orbit.body.non_rotating_reference_frame) 249 | global next_telem_time 250 | 251 | if time.time() > next_telem_time: 252 | display_telemetry(Telemetry(vessel, flight)) 253 | next_telem_time += TELEM_DELAY 254 | 255 | def display_telemetry(t): 256 | ''' 257 | Take a Telemetry object t and display it in a pleasing way 258 | ''' 259 | # define the data to be displayed in as many columns needed 260 | col1 = ('Apoapsis: {apoapsis:8,.0f}', 261 | 'Time to apo: {time_to_apo:5,.0f}', 262 | 'Altitude: {altitude:6,.0f}', 263 | 'Orbital velocity: {velocity:5,.0f}', 264 | 'Latitude: {lat:5.1f}', 265 | 'Dynamic Pressure: {q:6,.0f}') 266 | 267 | col2 = ('Periapsis: {periapsis: 8,.0f}', 268 | 'Time to peri: {time_to_peri:5,.0f}', 269 | 'Inclination: {inclination: 3.0f}\n', 270 | 'Vertical speed: {vertical_speed: 5,.0f}', 271 | 'Longitude: {lon:5.1f}\n', 272 | 'G-force: {g:4.1f}') 273 | # zip the columns together and display them 274 | print('-' * 60) 275 | for display_line in zip(col1, col2): 276 | print(' '.join(display_line).format(**t.__dict__)) 277 | print('-' * 60) 278 | print('\n') 279 | 280 | 281 | # ---------------------------------------------------------------------------- 282 | # Helper functions 283 | # ---------------------------------------------------------------------------- 284 | 285 | def still_in_atmosphere(conn): 286 | vessel = conn.space_center.active_vessel 287 | flight = vessel.flight(vessel.orbit.body.non_rotating_reference_frame) 288 | return flight.mean_altitude 0 and resource.amount(fueltype) == 0 321 | 322 | def next_stage(vessel): 323 | ''' 324 | activate the next stage 325 | ''' 326 | vessel.control.activate_next_stage() 327 | 328 | # ---------------------------------------------------------------------------- 329 | # Activate main loop, assuming we are executing THIS file explicitly. 330 | # ---------------------------------------------------------------------------- 331 | if __name__ == "__main__" : 332 | main() 333 | -------------------------------------------------------------------------------- /Art_Whaleys_KRPC_Demos/landing.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | ### Automated Landing Library and Example 3 | ###################################################################### 4 | ### Like all of the scripts in my folder here, this file contains 5 | ### functions you might want to include into your own scripts for 6 | ### actual use and a demo in the 'main' function that you can just 7 | ### run to see how it works. 8 | ### 9 | ### This file shows how to complete an automated landing. It 10 | ### does so in three phases - a deorbital burn, a suicide burn to a 11 | ### safety altitude and speed, and then a speed limited descent to 12 | ### final touch down. 13 | ###################################################################### 14 | 15 | import krpc 16 | import math 17 | import time 18 | import numpy as np 19 | 20 | from pid import PID 21 | 22 | ############################################################################## 23 | ### Main function - demonstrates the use of this library. Simply lands 24 | ### a vessel. Only propulsive landing - so really designed for planets 25 | ### without an atmosphere. 26 | ############################################################################## 27 | 28 | def main(): 29 | ''' Main function that demonstrates how to use the pieces of 30 | this landing library. 31 | 32 | Connects to KRPC server, performs a deorbit burn, then a 33 | suicide burn, then a final descent. 34 | ''' 35 | conn = krpc.connect() 36 | sc = conn.space_center 37 | v = sc.active_vessel 38 | 39 | 40 | deorbit(v, .3) 41 | suicide_burn(conn, v) 42 | final_descent(v) 43 | 44 | ############################################################################### 45 | ### High Level Functions 46 | ### 47 | ### These are the ones you'd actually maybe want to call from your script. 48 | ############################################################################### 49 | 50 | def deorbit(vessel, eccentricity): 51 | '''executes a deorbit burn until the given eccentricity 52 | is achieved. If periapsis remains above the equatorial 53 | radius, it continues burning. 54 | ''' 55 | print ("Deorbiting Vessel...") 56 | vessel.control.speed_mode = vessel.control.speed_mode.surface 57 | ap = vessel.auto_pilot 58 | ap.sas = True 59 | time.sleep(.1) 60 | ap.sas_mode = ap.sas_mode.retrograde 61 | 62 | while (vessel.orbit.eccentricity < eccentricity or 63 | vessel.orbit.periapsis_altitude > 0): 64 | vessel.control.throttle = 1.0 65 | vessel.control.throttle = 0.0 66 | 67 | def suicide_burn(conn, vessel, autowarp = True): 68 | ''' 69 | Performs a 'Suicide Burn' using the calculator class. 70 | ''' 71 | print "Calculating Suicide Burn..." 72 | telem = vessel.flight(vessel.orbit.body.reference_frame) 73 | vessel.control.speed_mode = vessel.control.speed_mode.surface 74 | rf = vessel.orbit.body.reference_frame 75 | ap = vessel.auto_pilot 76 | ap.sas = True 77 | time.sleep(.1) 78 | ap.sas_mode = ap.sas_mode.retrograde 79 | 80 | ##calculate initial burn 81 | computer = suicide_burn_calculator(conn, vessel, 5000) 82 | computer.update() # run once with altitude of 5000m to get an estimated groundtrack distance 83 | touchdown = coords_down_bearing(telem.latitude, telem.longitude, (180 + telem.heading),computer.ground_track,vessel.orbit.body) 84 | #update actual safe altitude over the groundtrack 85 | computer.alt = check_terrain(telem.latitude, telem.longitude, touchdown[0], touchdown[1], vessel.orbit.body) 86 | 87 | #Initial burn - aiming for the 'safe alt' (highest point over course) 88 | # and a vertical velocity of 10 m/s 89 | burn_until_velocity(conn, vessel, telem, 10, computer, True) 90 | 91 | ## update computer ti aim for 10m above current altitude. 92 | computer.alt = vessel.orbit.body.surface_height(telem.latitude, telem.longitude) + 10 93 | 94 | #Second half of burn - aiming for ground alt + 10m and a vertical 95 | #velocity of 5 m/s 96 | burn_until_velocity(conn, vessel, telem, 5, computer, True) 97 | 98 | def burn_until_velocity(conn, vessel, telem, thresh, computer, autowarp): 99 | ''' 100 | Helper Function for suicide_burn function - actually executes a burn 101 | at the calculated time and burns until it reaches the threshold 102 | vertical velocity. 103 | 104 | ''' 105 | countdown = computer.update() 106 | print ("Burn in {} seconds".format(countdown)) 107 | 108 | if autowarp and (countdown > thresh): 109 | conn.space_center.warp_to(conn.space_center.ut + countdown - 10) # warp to 10 seconds before burn 110 | 111 | while countdown > 0.0: # Wait until suicide burn 112 | time.sleep(.1) 113 | countdown = computer.update() 114 | 115 | while telem.vertical_speed < (-1 * thresh): #Loop until we're ready for final descent 116 | countdown = computer.update() 117 | vessel.control.throttle= .95 #95% throttle 118 | if countdown < 0.0: #use the emergencty 5% of throttle when needed 119 | vessel.control.throttle = 1.0 120 | vessel.control.throttle = 0.0 121 | 122 | def final_descent(v): 123 | ''' manages final descent. At the moment keeps vertical velocity limited to 1/10 of the 124 | current altitude above terrain - so at 200m would try to descend at 20m/s and at 10 m/s locks 125 | descent speed to 1m/s. 126 | ''' 127 | print "final descent" 128 | telem = v.flight(v.orbit.body.reference_frame) 129 | v.control.speed_mode = v.control.speed_mode.surface 130 | ap = v.auto_pilot 131 | ap.sas = True 132 | time.sleep(.1) 133 | ap.sas_mode = ap.sas_mode.retrograde 134 | p = PID(.25, .25, .025) 135 | while v.situation is not v.situation.landed: 136 | #ap.sas_mode = ap.sas_mode.retrograde # SAS leaves retrograde mode if velocity gets to zero. 137 | safe_descent = telem.surface_altitude / -10 138 | #print safe_descent 139 | if safe_descent < -15.0: 140 | safe_descent = -15.0 141 | p.setpoint(safe_descent) 142 | v.control.throttle = p.update(telem.vertical_speed) 143 | v.control.throttle = 0 144 | 145 | ############################################################################### 146 | ## Burn Calculator Class 147 | ############################################################################### 148 | 149 | 150 | class suicide_burn_calculator(object): 151 | ''' 152 | Class that calculates time until suicide burn. 153 | ''' 154 | def __init__(self, conn, v, alt): 155 | self.conn = conn 156 | self.v = v 157 | self.sc = conn.space_center 158 | self.burn_time = np.inf 159 | self.burn_duration = np.inf 160 | self.ground_track = np.inf 161 | self.effective_decel = np.inf 162 | self.radius = np.inf 163 | self.angle_from_horizontal = np.inf 164 | self.impact_time = np.inf 165 | self.alt = alt 166 | 167 | 168 | self.rf = self.sc.ReferenceFrame.create_hybrid( 169 | position=self.v.orbit.body.reference_frame, 170 | rotation=self.v.surface_reference_frame) 171 | 172 | def update(self): 173 | ''' 174 | Returns an estimate of how many seconds until you need to burn at 95% throttle to avoid crashing. 175 | This gives a 5% safety margin. 176 | I do not even PRETEND to understand all of the math in this function. It's essentially a porting 177 | of the routine from the Mechjeb orbit extensions. 178 | ''' 179 | 180 | if self.v.orbit.periapsis_altitude > 0: ## We're not on a landing trajectory yet. 181 | self.burn_time = np.inf 182 | self.burn_duration = np.inf 183 | self.ground_track = np.inf 184 | self.effective_decel = np.inf 185 | self.angle_from_horizontal = np.inf 186 | self.impact_time = np.inf 187 | return self.burn_time 188 | 189 | 190 | rf = self.v.orbit.body.reference_frame 191 | 192 | #calculate sin of angle from horizontal - 193 | v1 = self.v.velocity(self.rf) 194 | v2 = (0,0,1) 195 | self.angle_from_horizontal = angle_between(v1, v2) 196 | sine = math.sin(self.angle_from_horizontal) 197 | 198 | #estimate deceleration time 199 | g = self.v.orbit.body.surface_gravity 200 | T = (self.v.max_thrust / self.v.mass) *.95 # calculating with 5% safety margin! 201 | self.effective_decel = .5 * (-2 * g * sine + math.sqrt((2 * g * sine) * (2 * g * sine) + 4 * (T*T - g*g))) 202 | self.decel_time = self.v.flight(self.rf).speed / self.effective_decel 203 | 204 | #estimate time until burn 205 | radius = self.v.orbit.body.equatorial_radius + self.alt 206 | TA = self.v.orbit.true_anomaly_at_radius(radius) 207 | TA = -1 * TA #look on the negative (descending) side of the orbit 208 | self.impact_time = self.v.orbit.ut_at_true_anomaly(TA) 209 | self.burn_time = self.impact_time - self.decel_time/2 210 | self.ground_track = ((self.burn_time- self.sc.ut) * self.v.flight(self.rf).speed) + ( 211 | .5 * self.v.flight(self.rf).speed * self.decel_time) 212 | return self.burn_time - self.sc.ut 213 | 214 | 215 | 216 | 217 | 218 | ############################################################################### 219 | ## Ground Navigation Functions - Probably ought to move to their 220 | ## own library file some day. 221 | ############################################################################### 222 | 223 | def coords_down_bearing(lat, lon, bearing, distance, body): 224 | ''' 225 | Takes a latitude, longitude and bearing in degrees, and a 226 | distance in meters over a given body. Returns a tuple 227 | (latitude, longitude) of the point you've calculated. 228 | ''' 229 | bearing = math.radians(bearing) 230 | R = body.equatorial_radius 231 | lat = math.radians(lat) 232 | lon = math.radians(lon) 233 | 234 | lat2 = math.asin( math.sin(lat)*math.cos(distance/R) + 235 | math.cos(lat)*math.sin(distance/R)*math.cos(bearing)) 236 | 237 | lon2 = lon + math.atan2(math.sin(bearing)*math.sin(distance/R 238 | )*math.cos(lat),math.cos(distance/R)-math.sin(lat 239 | )*math.sin(lat2)) 240 | 241 | lat2 = math.degrees(lat2) 242 | lon2 = math.degrees(lon2) 243 | return (lat2, lon2) 244 | 245 | 246 | def check_terrain(lat1, lon1, lat2, lon2, body): 247 | ''' 248 | Returns an estimate of the highest terrain altitude betwen 249 | two latitude / longitude points. 250 | ''' 251 | lat = lat1 252 | lon = lon1 253 | highest_lat = lat 254 | highest_lon = lon 255 | highest_alt = body.surface_height(lat, lon) 256 | latstep = (lat2 - lat1) / 20 257 | lonstep = (lon2 - lon1) / 20 258 | 259 | for x in range (20): 260 | test_alt = body.surface_height(lat, lon) 261 | if test_alt> highest_alt: 262 | highest_lat = lat 263 | highest_lon = lon 264 | highest_alt = test_alt 265 | lat = lat+latstep 266 | lon = lon+lonstep 267 | return highest_alt 268 | 269 | 270 | 271 | ############################################################################### 272 | ## Vector Math Functions - Probably ought to move to their 273 | ## own library file some day. 274 | ############################################################################### 275 | 276 | def unit_vector(vector): 277 | """ Returns the unit vector of the vector provided. """ 278 | return vector / np.linalg.norm(vector) 279 | 280 | def angle_between(v1, v2): 281 | """ Returns the angle in radians between vectors 'v1' and 'v2'""" 282 | v1_u = unit_vector(v1) 283 | v2_u = unit_vector(v2) 284 | return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)) 285 | 286 | 287 | main() 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /examples/ksp-pynet/ksppynet/flight_plan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import asyncio 3 | import argparse 4 | import krpc 5 | import time 6 | import datetime 7 | import os 8 | import sys 9 | from ksppynet.flight_plan_node import ManeuverNode 10 | 11 | __all__ = ( 12 | "FlightPlan", 13 | ) 14 | 15 | class KStream(object): 16 | def __init__(self, conn, item, attr): 17 | self.stream = conn.add_stream(getattr, item, attr) 18 | 19 | def __call__(self): 20 | return self.stream() 21 | 22 | class FlightPlan(object): 23 | def msg(self, s, duration=5): 24 | print("{} <==> {}".format(datetime.datetime.now().isoformat(' '), s)) 25 | self.conn.ui.message(s, duration=duration) 26 | if self.debug_handler: 27 | self.debug_handler(s, duration=duration) 28 | 29 | 30 | def __init__(self, conn, vessel, debug_handler=None): 31 | self.debug_handler = debug_handler 32 | self.conn = conn 33 | self.vessel = vessel 34 | self.ap = vessel.auto_pilot 35 | self.autostaging_disabled = False 36 | # Defaults 37 | self.loop = asyncio.get_event_loop() 38 | self.attr = {} 39 | self.attr["ut"] = KStream(conn, conn.space_center, 'ut') 40 | self.attr["altitude"] = KStream(conn, vessel.flight(), 'mean_altitude') 41 | self.attr["vertical_speed"] = KStream(conn, vessel.flight(vessel.orbit.body.reference_frame), 'vertical_speed') 42 | self.attr["apoapsis"] = KStream(conn, vessel.orbit, 'apoapsis_altitude') 43 | self.attr["periapsis"] = KStream(conn, vessel.orbit, 'periapsis_altitude') 44 | self.attr["real_apoapsis"] = KStream(conn, vessel.orbit, 'apoapsis') 45 | self.attr["real_periapsis"] = KStream(conn, vessel.orbit, 'periapsis') 46 | self.attr["eccentricity"] = KStream(conn, vessel.orbit, 'eccentricity') 47 | 48 | self.sequence = asyncio.Queue() 49 | self.seq_defs = { 50 | "pre_launch" : self._pre_launch, 51 | "launch" : self._launcher, 52 | "deorbit" : self._deorbiter, 53 | "orbit" : self._orbiter, 54 | "land" : self._lander, 55 | "quit" : self._quit 56 | } 57 | 58 | def add_sequence(self, name, *args, **kwargs): 59 | self.msg("Appending sequence: {}".format(name)) 60 | asyncio.async(self.sequence.put((name, 61 | self.seq_defs[name], 62 | args, 63 | kwargs))) 64 | 65 | @asyncio.coroutine 66 | def _quit(self): 67 | self.loop.stop() 68 | 69 | @asyncio.coroutine 70 | def _start_sequence(self): 71 | while True: 72 | seq = yield from self.sequence.get() 73 | # Wait for the coroutine with its args/kwargs before dequeuing 74 | # the next in the sequence 75 | yield from asyncio.wait_for(seq[1](*seq[2], **seq[3]), None) 76 | 77 | @asyncio.coroutine 78 | def _autostager(self): 79 | while True: 80 | yield 81 | if self.autostaging_disabled: 82 | self.msg("Autostaging Disabled") 83 | return 84 | stage = self.vessel.control.current_stage 85 | parts = self.vessel.parts.in_stage(stage) 86 | for part in parts: 87 | if part.parachute: 88 | self.msg("Chutes in stage. Disabling autostaging") 89 | return 90 | 91 | parts = self.vessel.parts.in_decouple_stage(stage-1) 92 | fuel_in_stage = False 93 | for part in parts: 94 | engine = part.engine 95 | if engine and engine.active and engine.has_fuel: 96 | fuel_in_stage = True 97 | 98 | 99 | if not fuel_in_stage: 100 | self.msg("No fuel in stage. Staging...") 101 | self.vessel.control.activate_next_stage() 102 | else: 103 | yield from asyncio.sleep(0.2) 104 | 105 | @asyncio.coroutine 106 | def _launcher(self, altitude): 107 | self.msg("Executing Launch") 108 | self.desired_altitude = altitude 109 | self.turn_start_altitude = 250.0 110 | self.turn_mid_altitude = self.vessel.orbit.body.atmosphere_depth * 0.60 111 | self.turn_end_altitude = self.vessel.orbit.body.atmosphere_depth * 0.80 112 | def proportion(val, start, end): 113 | return (val - start) / (end - start) 114 | while True: 115 | yield 116 | altitude = self.attr["altitude"]() 117 | apoapsis = self.attr["apoapsis"]() 118 | if altitude < self.turn_start_altitude: 119 | self.ap.target_pitch_and_heading(90,self.desired_heading) 120 | elif self.turn_start_altitude <= altitude < self.turn_mid_altitude: 121 | #Only shallow out once we've got through the thicker part of the atmosphere. 122 | frac = proportion(altitude, 123 | self.turn_start_altitude, 124 | self.turn_mid_altitude) 125 | self.ap.target_pitch_and_heading(45 + 45*(1-frac),self.desired_heading) 126 | elif self.turn_mid_altitude <= altitude < self.turn_end_altitude: 127 | frac = proportion(altitude, 128 | self.turn_mid_altitude, 129 | self.turn_end_altitude) 130 | self.ap.target_pitch_and_heading(35*(1-frac)+5 ,self.desired_heading) 131 | else: 132 | self.ap.target_pitch_and_heading(5, self.desired_heading) 133 | 134 | if altitude > self.vessel.orbit.body.atmosphere_depth: 135 | fudge_factor = 1.0 136 | else: 137 | #Try and overshoot the desired altitude a little to account for resistence in the atmosphere 138 | fudge_factor = 1 + (self.vessel.orbit.body.atmosphere_depth - altitude) / (25 * self.vessel.orbit.body.atmosphere_depth) 139 | if apoapsis > self.desired_altitude * fudge_factor: 140 | self.vessel.control.throttle = 0 141 | if altitude > self.vessel.orbit.body.atmosphere_depth * 0.90: 142 | # Wait until we're mostly out of the atmosphere before setting maneuver nodes 143 | self.ap.disengage() 144 | return 145 | #else: control the throttle? 146 | 147 | @asyncio.coroutine 148 | def warp_to(self, target_ut, orig_warp_factor=4, lead_time=5): 149 | while True: 150 | yield 151 | warp_factor = orig_warp_factor 152 | if self.conn.space_center.rails_warp_factor != warp_factor: 153 | if not self.conn.space_center.can_rails_warp_at(warp_factor): 154 | warp_factor = self.conn.space_center.maximum_rails_warp_factor 155 | self.conn.space_center.rails_warp_factor = warp_factor 156 | 157 | ut = self.attr["ut"]() 158 | if ut > target_ut - lead_time: 159 | self.msg("Warp finished") 160 | self.drop_warp() 161 | return 162 | 163 | def drop_warp(self): 164 | self.conn.space_center.rails_warp_factor = 0 165 | self.conn.space_center.physics_warp_factor = 0 166 | 167 | @asyncio.coroutine 168 | def _pre_launch(self, heading): 169 | self.msg("Executing Prelaunch") 170 | self.desired_heading = heading 171 | self.vessel.control.sas = True # Is this ok? 172 | self.vessel.control.rcs = False 173 | self.vessel.control.throttle = 1 174 | self.ap.reference_frame = self.vessel.surface_reference_frame 175 | self.ap.target_pitch_and_heading(90, self.desired_heading) 176 | self.ap.target_roll = float('nan') 177 | self.ap.engage() 178 | 179 | @asyncio.coroutine 180 | def _deorbiter(self, periapsis, end_altitude): 181 | self.msg("Executing Deorbit") 182 | self.ap.reference_frame = self.vessel.orbital_reference_frame 183 | destage_altitude = self.vessel.orbit.body.atmosphere_depth * 0.90 184 | self.ap.target_direction = (0,-1,0) 185 | yield from asyncio.sleep(10) ## wait to turn. 186 | self.ap.engage() 187 | while True: 188 | yield 189 | cur_periapsis = self.attr["periapsis"]() 190 | self.ap.target_direction = (0,-1,0) 191 | if cur_periapsis > periapsis: 192 | self.vessel.control.throttle = 0.5 193 | else: 194 | self.vessel.control.throttle = 0 195 | break 196 | 197 | ut = self.attr["ut"]() 198 | self.loop.create_task(self.warp_to(ut + vessel.orbit.time_to_periapsis)) 199 | # The warp should stop in the atmosphere. 200 | while True: 201 | yield 202 | altitude = self.attr["altitude"]() 203 | if altitude < destage_altitude: 204 | break 205 | #disable warping 206 | self.drop_warp() 207 | self.msg("Turning") 208 | self.ap.target_direction = (0,-1,0) 209 | yield from asyncio.sleep(10) ## wait to turn. 210 | self.msg("Deceleration burn") 211 | self.vessel.control.throttle = 1 212 | yield from asyncio.sleep(20) ## Crude 213 | self.vessel.control.throttle = 0 214 | yield from asyncio.sleep(1) # Wait to check throttle is off before destaging 215 | self.autostaging_disabled = True 216 | 217 | chutes = False 218 | while not chutes: 219 | stage = self.vessel.control.current_stage 220 | parts = self.vessel.parts.in_stage(stage-1) 221 | for part in parts: 222 | if part.parachute: 223 | chutes = True 224 | if chutes: 225 | self.msg("Chutes in next stage.") 226 | else: 227 | self.msg("Destaging for landing") 228 | self.vessel.control.activate_next_stage() 229 | 230 | self.msg("Deorbit Complete, brace for landing!!") 231 | self.ap.disengage() 232 | 233 | def get_node(self): 234 | return ManeuverNode(self.conn, self.vessel, self.attr["ut"]) 235 | 236 | @asyncio.coroutine 237 | def _orbiter(self, apoapsis, periapsis): 238 | node = self.get_node() 239 | 240 | self.msg("Changing Periapsis to new Apoapsis {}".format(apoapsis)) 241 | node.change_periapsis(apoapsis) 242 | yield from asyncio.wait_for(node.execute(), None) 243 | 244 | self.msg("Changing new Periapsis to {}".format(periapsis)) 245 | node.change_apoapsis(periapsis) 246 | yield from asyncio.wait_for(node.execute(), None) 247 | 248 | @asyncio.coroutine 249 | def _lander(self, chute_altitude): 250 | self.msg("Executing Landing") 251 | self.ap.reference_frame = self.vessel.orbital_reference_frame 252 | self.ap.target_direction = (0,-1,0) 253 | self.ap.engage() 254 | while True: 255 | yield 256 | altitude = self.attr["altitude"]() 257 | self.ap.target_direction = (0,-1,0) 258 | if altitude < chute_altitude: 259 | while True: 260 | stage = self.vessel.control.current_stage 261 | parts = self.vessel.parts.in_stage(stage-1) 262 | self.vessel.control.activate_next_stage() 263 | for part in parts: 264 | if part.parachute: 265 | self.msg("Chute stage activated") 266 | return 267 | 268 | def run_sequence(self): 269 | self.loop.create_task(self._start_sequence()) 270 | 271 | def launch(self): 272 | # Start the sequence 273 | self.run_sequence() 274 | for i in range (5, 0, -1): 275 | self.msg("{} ...".format(i)) 276 | time.sleep(1) 277 | 278 | self.vessel.control.activate_next_stage() 279 | 280 | def set_autostaging(self): 281 | self.loop.create_task(self._autostager()) 282 | 283 | def close(self): 284 | self.conn.close() 285 | 286 | if __name__ == "__main__": 287 | 288 | parser = argparse.ArgumentParser(add_help=False) 289 | parser.add_argument('--apoapsis', type=int) 290 | parser.add_argument('--periapsis', type=int) 291 | parser.add_argument('--down', action='store_true') 292 | parser.add_argument('--diags', action='store_true') 293 | parser.add_argument('--test', nargs='+') 294 | args = parser.parse_args() 295 | 296 | 297 | conn = krpc.connect(name="{}.{}".format(os.path.basename(__file__), os.getpid())) 298 | vessel = conn.space_center.active_vessel 299 | 300 | fp = FlightPlan(conn, vessel) 301 | 302 | loop = asyncio.get_event_loop() 303 | def exit_on_exception(loop, context): 304 | print(context["message"]) 305 | if (not "exception" in context 306 | or not isinstance(context["exception"], ConnectionResetError)): 307 | loop.default_exception_handler(context) 308 | loop.stop() 309 | loop.set_exception_handler(exit_on_exception) 310 | 311 | if args.diags: 312 | #print("Vessel") 313 | #print(dir(vessel)) 314 | #print("Orbit") 315 | #print(dir(vessel.orbit)) 316 | #print("Periapsis:\nperiapsis: {}, altitude: {}".format(vessel.orbit.periapsis, vessel.orbit.periapsis_altitude)) 317 | #print("Apoapsis:\napoapsis: {}, altitude: {}".format(vessel.orbit.apoapsis, vessel.orbit.apoapsis_altitude)) 318 | #print(dir(vessel.orbit.body)) 319 | print("Altitude: {}, Destage at: {}, Vertical speed: {}".format(vessel.flight().mean_altitude, 320 | vessel.orbit.body.atmosphere_depth * 0.90, 321 | vessel.flight().vertical_speed)) 322 | print("Altitude: {}, Destage at: {}, Vertical speed: {}".format(vessel.flight().mean_altitude, 323 | vessel.orbit.body.atmosphere_depth * 0.90, 324 | vessel.flight(vessel.orbit.body.reference_frame).vertical_speed)) 325 | elif args.test: 326 | if args.test[0] == "warp": 327 | assert(args.test[1]) 328 | fp.msg("Warping ahead {} seconds".format(args.test[1])) 329 | loop.create_task(fp.warp_to(fp.attr["ut"]() + float(args.test[1]))) 330 | 331 | elif args.down: 332 | #fp.msg("Changing Apoapsis to {}".format(70000)) 333 | #node = fp.get_node() 334 | #node.change_apoapsis(70000) 335 | #loop.run_until_complete(node.execute()) 336 | fp.add_sequence("deorbit", periapsis=45000, end_altitude=4000) 337 | fp.add_sequence("land", chute_altitude=3000) 338 | fp.run_sequence() 339 | elif args.apoapsis: 340 | apoapsis = args.apoapsis 341 | node = fp.get_node() 342 | fp.msg("Changing Apoapsis to {}".format(apoapsis)) 343 | node.change_apoapsis(apoapsis) 344 | loop.create_task(node.execute()) 345 | elif args.periapsis: 346 | periapsis = args.periapsis 347 | node = fp.get_node() 348 | fp.msg("Changing Periapsis to {}".format(periapsis)) 349 | node.change_periapsis(periapsis) 350 | loop.create_task(node.execute()) 351 | else: 352 | fp.set_autostaging() 353 | 354 | fp.add_sequence("pre_launch", heading=90) 355 | fp.add_sequence("launch", altitude=80000) 356 | #fp.add_sequence("orbit", apoapsis=100000, periapsis=100000) 357 | fp.add_sequence("orbit", apoapsis=75000, periapsis=75000) 358 | fp.add_sequence("quit") 359 | #fp.add_sequence("deorbit", periapsis=45000, end_altitude=4000) 360 | #fp.add_sequence("land", chute_altitude=3000) 361 | 362 | fp.launch() 363 | 364 | pending = asyncio.Task.all_tasks() 365 | loop.run_until_complete(asyncio.gather(*pending)) 366 | --------------------------------------------------------------------------------