├── FreeSharedCockpitUDPSenderMaster.py ├── FreeSharedCockpitUDPSenderSlave.py ├── README.md ├── PI_FreeSharedCockpit_pluginMaster.py └── PI_FreeSharedCockpit_pluginSlave.py /FreeSharedCockpitUDPSenderMaster.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import pickle 3 | import threading 4 | import os 5 | 6 | #MASTER 7 | host = '192.168.1.102' 8 | inip = '0.0.0.0' 9 | inport = 49000 10 | outport = 49001 11 | 12 | 13 | '''SLAVE 14 | host = '192.168.1.115' 15 | inip = '0.0.0.0' 16 | inport = 49001 17 | outport = 49000 18 | ''' 19 | 20 | filename = 'C:/2.fll' 21 | try: 22 | os.remove(filename) 23 | except OSError: 24 | print('Cant remove file!') 25 | 26 | def udpRx(): 27 | while 1: 28 | try: 29 | infile = open('C:/1.fll', 'r') 30 | PickledTx = infile.read() 31 | except: 32 | print('read error') 33 | 34 | s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 35 | s1.connect((host, outport), ) 36 | s1.sendto(PickledTx, (host, outport)) 37 | s1.close() 38 | 39 | s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | s2.settimeout(0.04) 41 | try: 42 | s2.bind((inip, inport), ) 43 | PickledRx = s2.recvfrom(1024) 44 | try: 45 | outfile = open('C:/2.fll', 'w') 46 | outfile.write(PickledRx[0]) 47 | print(PickledRx[0]) 48 | except: 49 | print('Unable to write') 50 | except(socket.timeout, IndexError): 51 | print('UDP no data received from Port: ' + str(inport)) 52 | s2.close() 53 | 54 | 55 | udpthread = threading.Thread(target=udpRx).start() # export data and receive data every cycle 56 | 57 | -------------------------------------------------------------------------------- /FreeSharedCockpitUDPSenderSlave.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import pickle 3 | import threading 4 | import os 5 | 6 | '''MASTER 7 | host = '192.168.1.102' 8 | inip = '0.0.0.0' 9 | inport = 49000 10 | outport = 49001 11 | ''' 12 | 13 | #SLAVE 14 | host = '192.168.1.115' 15 | inip = '0.0.0.0' 16 | inport = 49001 17 | outport = 49000 18 | 19 | 20 | filename = 'C:/2.fll' 21 | try: 22 | os.remove(filename) 23 | except OSError: 24 | print('Cant remove file!') 25 | 26 | def udpRx(): 27 | while 1: 28 | try: 29 | infile = open('C:/1.fll', 'r') 30 | PickledTx = infile.read() 31 | except: 32 | print('read error') 33 | 34 | s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 35 | s1.connect((host, outport), ) 36 | s1.sendto(PickledTx, (host, outport)) 37 | s1.close() 38 | 39 | s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | s2.settimeout(0.04) 41 | try: 42 | s2.bind((inip, inport), ) 43 | PickledRx = s2.recvfrom(1024) 44 | try: 45 | outfile = open('C:/2.fll', 'w') 46 | outfile.write(PickledRx[0]) 47 | print(PickledRx[0]) 48 | except: 49 | print('Unable to write') 50 | except(socket.timeout, IndexError): 51 | print('UDP no data received from Port: ' + str(inport)) 52 | s2.close() 53 | 54 | 55 | udpthread = threading.Thread(target=udpRx).start() # export data and receive data every cycle 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeSharedCockpit 2 | An open source free XPlane 11 Shared cockpit plugin development 3 | 4 | Hi Guys, 5 | 6 | I’m trying to develop a free and open source shared cockpit software that works with XP11. I have written my own python script to synchronise Xplane Datarefs across 2 computers over UDP protocol. 7 | 8 | I know that there are alternatives out there for shared cockpit already, and the reason why I am doing this is because: 9 | 1) JoinFS doesn't work for shared cockpit on XPlane 11, trust me, I've tried everything to make it work... 10 | 2) Smartcopilot is way too expensive for single license 11 | 3) Teamviewer and Discord Screenshare does not provide a shared cockpit experience 12 | 4) An open source, free for all shared cockpit software that allows customisation would allow much more people to benefit from it, without paying a single cent. 13 | 14 | The main target group of people that this program would potentially benefit are: 15 | 1) Folks who play XPlane offline, but are too scared to go on VATSIM online. Flying shared cockpit where the instructor and student are in the same plane will allow splitting of the workload, making it a less daunting task - I would certainly love to have an instructor with me on my first VATSIM flight! 16 | 2) Folks who are training for their VATSIM P ratings - discord screenshare or teamviewer is just inferior to a shared cockpit environment 17 | 3) Folks who just want to have fun flying together in a crew 18 | 19 | An XPlane shared cockpit software is essentially a piece of software that syncs Xplane datarefs between 2 computers, such that moving the controls/switches on one computer causes a synchronised change on the other. 20 | 21 | As can be seen in the video below: 22 | 23 | https://youtu.be/luqvsnya1GA 24 | 25 | A big step forward now compared to the previous, aircraft attitude, heading and position are now synced exactly from master to slave. Again, the same setup, the laptop on the left is the slave computer, having the plane mirrored to the master Desktop on the right. Both sceneries look different because my laptop can only run on minimal settings while my desktop is running on high and you can see the ground features much better. 26 | 27 | Next steps would be to: 28 | 1) Create a single common script file for both master and slave computers 29 | 2) Allow toggle of control inputs between master and slave, the toggle button (on the master computer) should: 30 | - Slave: Enable the physics model 31 | - Master: Disable the physics model 32 | - Slave: Stop setting aircraft position, attitude, velocities from the master 33 | - Master: Set aircraft position, attitude, velocities from the slave 34 | 3) Untoggling the switch should perform the above but in reverse 35 | 4) Create a GUI for the toggle button, Slave/Master IP address and Ports input box, connect button 36 | 5) Sync the engine on/off state 37 | 6) Sync the cockpit/autopilot switches 38 | 7) Optimise for speed - cut down precision of variables, code cleanup 39 | 8) Write an instruction manual - required dependencies etc... 40 | 41 | >>> Once we reach this stage, it will be time to publish an early beta of the software! 42 | 43 | 44 | 45 | How to use: 46 | 1) Edit both files FreeSharedCockpitUDPSenderMaster.py and FreeSharedCockpitUDPSenderSlave.py and change the ip address of the "host" variable to the IP address of the other computer you are trying to connect to. (i.e: Master should input the slave's IP and Slave should input the Master's IP) 47 | 2) The PI_FreeSharedCockpit_pluginMaster.py script is to be placed in the PythonScripts folder under Xplane plugins folder on the Master computer, and the corresponding PI_FreeSharedCockpit_pluginSlave.py in the Xplane plugins folder on the Slave computer. 48 | 3) Run the FreeSharedCockpitUDPSenderMaster.py script using the command line on the master computer and FreeSharedCockpitUDPSenderSlave.py on the slave computer. 49 | 4) Load up Xplane on both master and slave at same airport (does not need to be the exact same location at the airport) and make sure that both have the exact same scenery 50 | 5) On the slave computer, click on the dialog box once to start syncing the position of the slave to the master computer, the state of the aircraft on the slave computer will snap to reflect that on the master computer. Click the dialog box again to unsync. 51 | 6) Start flying on the master computer, the aircraft on the slave computer will be synced! 52 | 53 | 54 | 55 | Anyone keen to help me in this project please feel free to contribute! Thanks for viewing! 56 | -------------------------------------------------------------------------------- /PI_FreeSharedCockpit_pluginMaster.py: -------------------------------------------------------------------------------- 1 | from XPLMDefs import * 2 | from XPLMDataAccess import * 3 | from XPLMDisplay import * 4 | from XPLMGraphics import * 5 | from XPLMUtilities import * 6 | import pickle 7 | import os 8 | 9 | DataRx = [] 10 | sync = False 11 | 12 | filename = 'C:/2.fll' 13 | try: 14 | os.remove(filename) 15 | except OSError: 16 | print('Cant remove file!') 17 | 18 | class PythonInterface: 19 | 20 | def XPluginStart(self): 21 | self.Name = "Test" 22 | self.Sig = "Ong.Python.Test" 23 | self.Desc = "Datarefs" 24 | self.Clicked = 0 25 | self.DrawWindowCB = self.DrawWindowCallback 26 | self.KeyCB = self.KeyCallback 27 | self.MouseClickCB = self.MouseClickCallback 28 | self.WindowId = XPLMCreateWindow(self, 50, 600, 400, 570, 1, self.DrawWindowCB, self.KeyCB, self.MouseClickCB, 0) 29 | return self.Name, self.Sig, self.Desc 30 | 31 | def XPluginStop(self): 32 | XPLMDestroyWindow(self, self.WindowId) 33 | pass 34 | 35 | def XPluginEnable(self): 36 | return 1 37 | 38 | def XPluginDisable(self): 39 | pass 40 | 41 | def XPluginReceiveMessage(self, inFromWho, inMessage, inParam): 42 | pass 43 | 44 | def DrawWindowCallback(self, inWindowID, inRefcon): 45 | # First we get the location of the window passed in to us. 46 | lLeft = []; lTop = []; lRight = []; lBottom = [] 47 | XPLMGetWindowGeometry(inWindowID, lLeft, lTop, lRight, lBottom) 48 | left = int(lLeft[0]); top = int(lTop[0]); right = int(lRight[0]); bottom = int(lBottom[0]) 49 | XPLMDrawTranslucentDarkBox(left, top, right, bottom) 50 | color = 1.0, 1.0, 1.0 51 | 52 | # Access in sim variables 53 | AccessorDataRefX = XPLMFindDataRef("sim/flightmodel/position/local_x") 54 | DataX = XPLMGetDataf(AccessorDataRefX) 55 | AccessorDataRefY = XPLMFindDataRef("sim/flightmodel/position/local_y") 56 | DataY = XPLMGetDataf(AccessorDataRefY) 57 | AccessorDataRefZ = XPLMFindDataRef("sim/flightmodel/position/local_z") 58 | DataZ = XPLMGetDataf(AccessorDataRefZ) 59 | AccessorDataRefPsi = XPLMFindDataRef("sim/flightmodel/position/psi") 60 | DataPsi = XPLMGetDataf(AccessorDataRefPsi) 61 | AccessorDataRefTheta = XPLMFindDataRef("sim/flightmodel/position/theta") 62 | DataTheta = XPLMGetDataf(AccessorDataRefTheta) 63 | AccessorDataRefPhi = XPLMFindDataRef("sim/flightmodel/position/phi") 64 | DataPhi = XPLMGetDataf(AccessorDataRefPhi) 65 | AccessorDataRefP = XPLMFindDataRef("sim/flightmodel/position/P") 66 | DataP = XPLMGetDataf(AccessorDataRefP) 67 | AccessorDataRefQ = XPLMFindDataRef("sim/flightmodel/position/Q") 68 | DataQ = XPLMGetDataf(AccessorDataRefQ) 69 | AccessorDataRefR = XPLMFindDataRef("sim/flightmodel/position/R") 70 | DataR = XPLMGetDataf(AccessorDataRefR) 71 | AccessorDataRefVX = XPLMFindDataRef("sim/flightmodel/position/local_vx") 72 | DataVX = XPLMGetDataf(AccessorDataRefVX) 73 | AccessorDataRefVY = XPLMFindDataRef("sim/flightmodel/position/local_vy") 74 | DataVY = XPLMGetDataf(AccessorDataRefVY) 75 | AccessorDataRefVZ = XPLMFindDataRef("sim/flightmodel/position/local_vz") 76 | DataVZ = XPLMGetDataf(AccessorDataRefVZ) 77 | 78 | # Export in sim data to other comp 79 | global DataRx 80 | DataTx = [DataX, DataY, DataZ, DataPsi, DataTheta, DataPhi, DataP, DataQ, DataR, DataVX, DataVY, DataVZ] 81 | outfile = open('C:/1.fll', 'w') 82 | pickle.dump(DataTx, outfile) 83 | outfile.close() 84 | 85 | try: 86 | infile = open('C:/2.fll', 'r') 87 | DataRx = pickle.load(infile) 88 | except: 89 | print('IOError') 90 | 91 | 92 | 93 | # Common script for both master and slave 94 | DescTx = 'Tx: ' + str(DataTx) 95 | DescRx = 'Rx: ' + str(DataRx) 96 | XPLMDrawString(color, left + 5, top - 10, DescTx, 0, xplmFont_Basic) 97 | XPLMDrawString(color, left + 5, top - 20, DescRx, 0, xplmFont_Basic) 98 | pass 99 | 100 | def KeyCallback(self, inWindowID, inKey, inFlags, inVirtualKey, inRefcon, losingFocus): 101 | pass 102 | 103 | def MouseClickCallback(self, inWindowID, x, y, inMouse, inRefcon): 104 | if (inMouse == xplm_MouseDown) or (inMouse == xplm_MouseUp): 105 | self.Clicked = 1 - self.Clicked 106 | return 1 107 | -------------------------------------------------------------------------------- /PI_FreeSharedCockpit_pluginSlave.py: -------------------------------------------------------------------------------- 1 | from XPLMDefs import * 2 | from XPLMDataAccess import * 3 | from XPLMDisplay import * 4 | from XPLMGraphics import * 5 | from XPLMUtilities import * 6 | import pickle 7 | import os 8 | 9 | DataRx = [] 10 | sync = False 11 | 12 | filename = 'C:/2.fll' 13 | try: 14 | os.remove(filename) 15 | except OSError: 16 | print('Cant remove file!') 17 | 18 | class PythonInterface: 19 | 20 | def XPluginStart(self): 21 | self.Name = "Test" 22 | self.Sig = "Ong.Python.Test" 23 | self.Desc = "Datarefs" 24 | self.Clicked = 0 25 | self.DrawWindowCB = self.DrawWindowCallback 26 | self.KeyCB = self.KeyCallback 27 | self.MouseClickCB = self.MouseClickCallback 28 | self.WindowId = XPLMCreateWindow(self, 50, 600, 400, 570, 1, self.DrawWindowCB, self.KeyCB, self.MouseClickCB, 0) 29 | return self.Name, self.Sig, self.Desc 30 | 31 | def XPluginStop(self): 32 | XPLMDestroyWindow(self, self.WindowId) 33 | pass 34 | 35 | def XPluginEnable(self): 36 | return 1 37 | 38 | def XPluginDisable(self): 39 | pass 40 | 41 | def XPluginReceiveMessage(self, inFromWho, inMessage, inParam): 42 | pass 43 | 44 | def DrawWindowCallback(self, inWindowID, inRefcon): 45 | # First we get the location of the window passed in to us. 46 | lLeft = []; lTop = []; lRight = []; lBottom = [] 47 | XPLMGetWindowGeometry(inWindowID, lLeft, lTop, lRight, lBottom) 48 | left = int(lLeft[0]); top = int(lTop[0]); right = int(lRight[0]); bottom = int(lBottom[0]) 49 | XPLMDrawTranslucentDarkBox(left, top, right, bottom) 50 | color = 1.0, 1.0, 1.0 51 | 52 | # Access in sim variables 53 | AccessorDataRefX = XPLMFindDataRef("sim/flightmodel/position/local_x") 54 | DataX = XPLMGetDataf(AccessorDataRefX) 55 | AccessorDataRefY = XPLMFindDataRef("sim/flightmodel/position/local_y") 56 | DataY = XPLMGetDataf(AccessorDataRefY) 57 | AccessorDataRefZ = XPLMFindDataRef("sim/flightmodel/position/local_z") 58 | DataZ = XPLMGetDataf(AccessorDataRefZ) 59 | AccessorDataRefPsi = XPLMFindDataRef("sim/flightmodel/position/psi") 60 | DataPsi = XPLMGetDataf(AccessorDataRefPsi) 61 | AccessorDataRefTheta = XPLMFindDataRef("sim/flightmodel/position/theta") 62 | DataTheta = XPLMGetDataf(AccessorDataRefTheta) 63 | AccessorDataRefPhi = XPLMFindDataRef("sim/flightmodel/position/phi") 64 | DataPhi = XPLMGetDataf(AccessorDataRefPhi) 65 | AccessorDataRefP = XPLMFindDataRef("sim/flightmodel/position/P") 66 | DataP = XPLMGetDataf(AccessorDataRefP) 67 | AccessorDataRefQ = XPLMFindDataRef("sim/flightmodel/position/Q") 68 | DataQ = XPLMGetDataf(AccessorDataRefQ) 69 | AccessorDataRefR = XPLMFindDataRef("sim/flightmodel/position/R") 70 | DataR = XPLMGetDataf(AccessorDataRefR) 71 | AccessorDataRefVX = XPLMFindDataRef("sim/flightmodel/position/local_vx") 72 | DataVX = XPLMGetDataf(AccessorDataRefVX) 73 | AccessorDataRefVY = XPLMFindDataRef("sim/flightmodel/position/local_vy") 74 | DataVY = XPLMGetDataf(AccessorDataRefVY) 75 | AccessorDataRefVZ = XPLMFindDataRef("sim/flightmodel/position/local_vz") 76 | DataVZ = XPLMGetDataf(AccessorDataRefVZ) 77 | 78 | # Export in sim data to other comp 79 | global DataRx 80 | DataTx = [DataX, DataY, DataZ, DataPsi, DataTheta, DataPhi, DataP, DataQ, DataR, DataVX, DataVY, DataVZ] 81 | outfile = open('C:/1.fll', 'w') 82 | pickle.dump(DataTx, outfile) 83 | outfile.close() 84 | 85 | try: 86 | infile = open('C:/2.fll', 'r') 87 | DataRx = pickle.load(infile) 88 | except: 89 | pass 90 | 91 | # Slave specific script 92 | # Sync the position of the slave to the master when box is clicked 93 | if self.Clicked: 94 | global sync 95 | sync = not sync 96 | print(sync) 97 | if sync == False: 98 | PhysicsRef = XPLMFindDataRef("sim/operation/override/override_planepath") 99 | ovrd_Vals = [0] 100 | XPLMSetDatavi(PhysicsRef, ovrd_Vals, 0, 1) 101 | 102 | if sync == True: 103 | try: 104 | PhysicsRef = XPLMFindDataRef("sim/operation/override/override_planepath") 105 | ovrd_Vals = [1] 106 | XPLMSetDatavi(PhysicsRef, ovrd_Vals, 0, 1) 107 | XPLMSetDataf(AccessorDataRefX, DataRx[0]) 108 | XPLMSetDataf(AccessorDataRefY, DataRx[1]) 109 | XPLMSetDataf(AccessorDataRefZ, DataRx[2]) 110 | XPLMSetDataf(AccessorDataRefPsi, DataRx[3]) 111 | XPLMSetDataf(AccessorDataRefTheta, DataRx[4]) 112 | XPLMSetDataf(AccessorDataRefPhi, DataRx[5]) 113 | XPLMSetDataf(AccessorDataRefP, DataRx[6]) 114 | XPLMSetDataf(AccessorDataRefQ, DataRx[7]) 115 | XPLMSetDataf(AccessorDataRefR, DataRx[8]) 116 | XPLMSetDataf(AccessorDataRefVX, DataRx[9]) 117 | XPLMSetDataf(AccessorDataRefVY, DataRx[10]) 118 | XPLMSetDataf(AccessorDataRefVZ, DataRx[11]) 119 | 120 | except: 121 | pass 122 | else: 123 | pass 124 | 125 | 126 | 127 | # Common script for both master and slave 128 | DescTx = 'Tx: ' + str(DataTx) 129 | DescRx = 'Rx: ' + str(DataRx) 130 | XPLMDrawString(color, left + 5, top - 10, DescTx, 0, xplmFont_Basic) 131 | XPLMDrawString(color, left + 5, top - 20, DescRx, 0, xplmFont_Basic) 132 | pass 133 | 134 | def KeyCallback(self, inWindowID, inKey, inFlags, inVirtualKey, inRefcon, losingFocus): 135 | pass 136 | 137 | def MouseClickCallback(self, inWindowID, x, y, inMouse, inRefcon): 138 | if (inMouse == xplm_MouseDown) or (inMouse == xplm_MouseUp): 139 | self.Clicked = 1 - self.Clicked 140 | return 1 141 | --------------------------------------------------------------------------------