fgFDM.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env python
  2. # parse and construct FlightGear NET FDM packets
  3. # Andrew Tridgell, November 2011
  4. # released under GNU GPL version 2 or later
  5. from builtins import range
  6. from builtins import object
  7. import struct, math
  8. class fgFDMError(Exception):
  9. '''fgFDM error class'''
  10. def __init__(self, msg):
  11. Exception.__init__(self, msg)
  12. self.message = 'fgFDMError: ' + msg
  13. class fgFDMVariable(object):
  14. '''represent a single fgFDM variable'''
  15. def __init__(self, index, arraylength, units):
  16. self.index = index
  17. self.arraylength = arraylength
  18. self.units = units
  19. class fgFDMVariableList(object):
  20. '''represent a list of fgFDM variable'''
  21. def __init__(self):
  22. self.vars = {}
  23. self._nextidx = 0
  24. def add(self, varname, arraylength=1, units=None):
  25. self.vars[varname] = fgFDMVariable(self._nextidx, arraylength, units=units)
  26. self._nextidx += arraylength
  27. class fgFDM(object):
  28. '''a flightgear native FDM parser/generator'''
  29. def __init__(self):
  30. '''init a fgFDM object'''
  31. self.FG_NET_FDM_VERSION = 24
  32. self.pack_string = '>I 4x 3d 6f 11f 3f 2f I 4I 4f 4f 4f 4f 4f 4f 4f 4f 4f I 4f I 3I 3f 3f 3f I i f 10f'
  33. self.values = [0]*98
  34. self.FG_MAX_ENGINES = 4
  35. self.FG_MAX_WHEELS = 3
  36. self.FG_MAX_TANKS = 4
  37. # supported unit mappings
  38. self.unitmap = {
  39. ('radians', 'degrees') : math.degrees(1),
  40. ('rps', 'dps') : math.degrees(1),
  41. ('feet', 'meters') : 0.3048,
  42. ('fps', 'mps') : 0.3048,
  43. ('knots', 'mps') : 0.514444444,
  44. ('knots', 'fps') : 0.514444444/0.3048,
  45. ('fpss', 'mpss') : 0.3048,
  46. ('seconds', 'minutes') : 60,
  47. ('seconds', 'hours') : 3600,
  48. }
  49. # build a mapping between variable name and index in the values array
  50. # note that the order of this initialisation is critical - it must
  51. # match the wire structure
  52. self.mapping = fgFDMVariableList()
  53. self.mapping.add('version')
  54. # position
  55. self.mapping.add('longitude', units='radians') # geodetic (radians)
  56. self.mapping.add('latitude', units='radians') # geodetic (radians)
  57. self.mapping.add('altitude', units='meters') # above sea level (meters)
  58. self.mapping.add('agl', units='meters') # above ground level (meters)
  59. # attitude
  60. self.mapping.add('phi', units='radians') # roll (radians)
  61. self.mapping.add('theta', units='radians') # pitch (radians)
  62. self.mapping.add('psi', units='radians') # yaw or true heading (radians)
  63. self.mapping.add('alpha', units='radians') # angle of attack (radians)
  64. self.mapping.add('beta', units='radians') # side slip angle (radians)
  65. # Velocities
  66. self.mapping.add('phidot', units='rps') # roll rate (radians/sec)
  67. self.mapping.add('thetadot', units='rps') # pitch rate (radians/sec)
  68. self.mapping.add('psidot', units='rps') # yaw rate (radians/sec)
  69. self.mapping.add('vcas', units='fps') # calibrated airspeed
  70. self.mapping.add('climb_rate', units='fps') # feet per second
  71. self.mapping.add('v_north', units='fps') # north velocity in local/body frame, fps
  72. self.mapping.add('v_east', units='fps') # east velocity in local/body frame, fps
  73. self.mapping.add('v_down', units='fps') # down/vertical velocity in local/body frame, fps
  74. self.mapping.add('v_wind_body_north', units='fps') # north velocity in local/body frame
  75. self.mapping.add('v_wind_body_east', units='fps') # east velocity in local/body frame
  76. self.mapping.add('v_wind_body_down', units='fps') # down/vertical velocity in local/body
  77. # Accelerations
  78. self.mapping.add('A_X_pilot', units='fpss') # X accel in body frame ft/sec^2
  79. self.mapping.add('A_Y_pilot', units='fpss') # Y accel in body frame ft/sec^2
  80. self.mapping.add('A_Z_pilot', units='fpss') # Z accel in body frame ft/sec^2
  81. # Stall
  82. self.mapping.add('stall_warning') # 0.0 - 1.0 indicating the amount of stall
  83. self.mapping.add('slip_deg', units='degrees') # slip ball deflection
  84. # Engine status
  85. self.mapping.add('num_engines') # Number of valid engines
  86. self.mapping.add('eng_state', self.FG_MAX_ENGINES) # Engine state (off, cranking, running)
  87. self.mapping.add('rpm', self.FG_MAX_ENGINES) # Engine RPM rev/min
  88. self.mapping.add('fuel_flow', self.FG_MAX_ENGINES) # Fuel flow gallons/hr
  89. self.mapping.add('fuel_px', self.FG_MAX_ENGINES) # Fuel pressure psi
  90. self.mapping.add('egt', self.FG_MAX_ENGINES) # Exhuast gas temp deg F
  91. self.mapping.add('cht', self.FG_MAX_ENGINES) # Cylinder head temp deg F
  92. self.mapping.add('mp_osi', self.FG_MAX_ENGINES) # Manifold pressure
  93. self.mapping.add('tit', self.FG_MAX_ENGINES) # Turbine Inlet Temperature
  94. self.mapping.add('oil_temp', self.FG_MAX_ENGINES) # Oil temp deg F
  95. self.mapping.add('oil_px', self.FG_MAX_ENGINES) # Oil pressure psi
  96. # Consumables
  97. self.mapping.add('num_tanks') # Max number of fuel tanks
  98. self.mapping.add('fuel_quantity', self.FG_MAX_TANKS)
  99. # Gear status
  100. self.mapping.add('num_wheels')
  101. self.mapping.add('wow', self.FG_MAX_WHEELS)
  102. self.mapping.add('gear_pos', self.FG_MAX_WHEELS)
  103. self.mapping.add('gear_steer', self.FG_MAX_WHEELS)
  104. self.mapping.add('gear_compression', self.FG_MAX_WHEELS)
  105. # Environment
  106. self.mapping.add('cur_time', units='seconds') # current unix time
  107. self.mapping.add('warp', units='seconds') # offset in seconds to unix time
  108. self.mapping.add('visibility', units='meters') # visibility in meters (for env. effects)
  109. # Control surface positions (normalized values)
  110. self.mapping.add('elevator')
  111. self.mapping.add('elevator_trim_tab')
  112. self.mapping.add('left_flap')
  113. self.mapping.add('right_flap')
  114. self.mapping.add('left_aileron')
  115. self.mapping.add('right_aileron')
  116. self.mapping.add('rudder')
  117. self.mapping.add('nose_wheel')
  118. self.mapping.add('speedbrake')
  119. self.mapping.add('spoilers')
  120. self._packet_size = struct.calcsize(self.pack_string)
  121. self.set('version', self.FG_NET_FDM_VERSION)
  122. if len(self.values) != self.mapping._nextidx:
  123. raise fgFDMError('Invalid variable list in initialisation')
  124. def packet_size(self):
  125. '''return expected size of FG FDM packets'''
  126. return self._packet_size
  127. def convert(self, value, fromunits, tounits):
  128. '''convert a value from one set of units to another'''
  129. if fromunits == tounits:
  130. return value
  131. if (fromunits,tounits) in self.unitmap:
  132. return value * self.unitmap[(fromunits,tounits)]
  133. if (tounits,fromunits) in self.unitmap:
  134. return value / self.unitmap[(tounits,fromunits)]
  135. raise fgFDMError("unknown unit mapping (%s,%s)" % (fromunits, tounits))
  136. def units(self, varname):
  137. '''return the default units of a variable'''
  138. if not varname in self.mapping.vars:
  139. raise fgFDMError('Unknown variable %s' % varname)
  140. return self.mapping.vars[varname].units
  141. def variables(self):
  142. '''return a list of available variables'''
  143. return sorted(list(self.mapping.vars.keys()),
  144. key = lambda v : self.mapping.vars[v].index)
  145. def get(self, varname, idx=0, units=None):
  146. '''get a variable value'''
  147. if not varname in self.mapping.vars:
  148. raise fgFDMError('Unknown variable %s' % varname)
  149. if idx >= self.mapping.vars[varname].arraylength:
  150. raise fgFDMError('index of %s beyond end of array idx=%u arraylength=%u' % (
  151. varname, idx, self.mapping.vars[varname].arraylength))
  152. value = self.values[self.mapping.vars[varname].index + idx]
  153. if units:
  154. value = self.convert(value, self.mapping.vars[varname].units, units)
  155. return value
  156. def set(self, varname, value, idx=0, units=None):
  157. '''set a variable value'''
  158. if not varname in self.mapping.vars:
  159. raise fgFDMError('Unknown variable %s' % varname)
  160. if idx >= self.mapping.vars[varname].arraylength:
  161. raise fgFDMError('index of %s beyond end of array idx=%u arraylength=%u' % (
  162. varname, idx, self.mapping.vars[varname].arraylength))
  163. if units:
  164. value = self.convert(value, units, self.mapping.vars[varname].units)
  165. # avoid range errors when packing into 4 byte floats
  166. if math.isinf(value) or math.isnan(value) or math.fabs(value) > 3.4e38:
  167. value = 0
  168. self.values[self.mapping.vars[varname].index + idx] = value
  169. def parse(self, buf):
  170. '''parse a FD FDM buffer'''
  171. try:
  172. t = struct.unpack(self.pack_string, buf)
  173. except struct.error as msg:
  174. raise fgFDMError('unable to parse - %s' % msg)
  175. self.values = list(t)
  176. def pack(self):
  177. '''pack a FD FDM buffer from current values'''
  178. for i in range(len(self.values)):
  179. if math.isnan(self.values[i]):
  180. self.values[i] = 0
  181. return struct.pack(self.pack_string, *self.values)