Changeset 4:bcc93ecb8cb6


Ignore:
Timestamp:
01/29/12 14:00:30 (13 years ago)
Author:
István Váradi <ivaradi@…>
Branch:
default
Phase:
public
Message:

The aircraft model framework is basically implemented

Location:
src
Files:
4 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/fs.py

    r3 r4  
    11# Module for generic flight-simulator interfaces
    22
    3 # Flight simulator type: MS Flight Simulator 2004
    4 TYPE_FS2K4 = 1
     3#-------------------------------------------------------------------------------
    54
    6 # Flight simulator type: MS Flight Simulator X
    7 TYPE_FSX = 2
     5import const
    86
    9 # Flight simulator type: X-Plane 9
    10 TYPE_XPLANE9 = 3
     7#-------------------------------------------------------------------------------
    118
    12 # Flight simulator type: X-Plane 10
    13 TYPE_XPLANE10 = 4
    14 
    15 class ConnectionListener:
     9class ConnectionListener(object):
    1610    """Base class for listeners on connections to the flight simulator."""
    1711    def connected(self, fsType, descriptor):
     
    2418        print "fs.ConnectionListener.disconnected"
    2519
     20#-------------------------------------------------------------------------------
     21
     22class SimulatorException(Exception):
     23    """Exception thrown by the simulator interface for communication failure."""
     24
     25#-------------------------------------------------------------------------------
     26
     27def createSimulator(type, connectionListener):
     28    """Create a simulator instance for the given simulator type with the given
     29    connection listener.
     30
     31    The returned object should provide the following members:
     32    FIXME: add info
     33    """
     34    assert type==const.TYPE_MSFS9, "Only MS Flight Simulator 2004 is supported"
     35    import fsuipc
     36    return fsuipc.Simulator(connectionListener)
     37
     38#-------------------------------------------------------------------------------
     39
     40class AircraftState(object):
     41    """Base class for the aircraft state produced by the aircraft model based
     42    on readings from the simulator.
     43
     44    The following data members should be provided at least:
     45    - timestamp: the simulator time of the measurement in seconds since the
     46    epoch (float)
     47    - paused: a boolean indicating if the flight simulator is paused for
     48    whatever reason (it could be a pause mode, or a menu, a dialog, or a
     49    replay, etc.)
     50    - trickMode: a boolean indicating if some "trick" mode (e.g. "SLEW" in
     51    MSFS) is activated
     52    - overspeed: a boolean indicating if the aircraft is in overspeed
     53    - stalled: a boolean indicating if the aircraft is stalled
     54    - onTheGround: a boolean indicating if the aircraft is on the ground
     55    - grossWeight: the gross weight in kilograms (float)
     56    - heading: the heading of the aircraft in degrees (float)
     57    - pitch: the pitch of the aircraft in degrees. Positive means pitch down,
     58    negative means pitch up (float)
     59    - bank: the bank of the aircraft in degrees. Positive means bank left,
     60    negative means bank right (float)
     61    - ias: the indicated airspeed in knots (float)   
     62    - vs: the vertical speed in feet/minutes (float)
     63    - altitude: the altitude of the aircraft in feet (float)
     64    - flapsSet: the selected degrees of the flaps (float)
     65    - flaps: the actual degrees of the flaps (float)
     66    - n1[]: the N1 values of the turbine engines (array of floats
     67    of as many items as the number of engines, present only for aircraft with
     68    turbines)
     69    - rpm[]: the RPM values of the piston engines (array of floats
     70    of as many items as the number of engines, present only for aircraft with
     71    pistons)
     72    - reverser[]: an array of booleans indicating if the thrust reversers are
     73    activated on any of the engines. The number of items equals to the number
     74    of engines.
     75    - navLightsOn: a boolean indicating if the navigation lights are on
     76    - antiCollisionLightsOn: a boolean indicating if the anti-collision lights are on
     77    - strobeLightsOn: a boolean indicating if the strobe lights are on
     78    - langingLightsOn: a boolean indicating if the landing lights are on
     79    - pitotHeatOn: a boolean indicating if the pitot heat is on
     80    - gearsDown: a boolean indicating if the gears are down
     81    - spoilersArmed: a boolean indicating if the spoilers have been armed for
     82    automatic deployment
     83    - spoilersExtension: the percentage of how much the spoiler is extended
     84    (float)
     85
     86    FIXME: needed when taxiing only:
     87    - zfw: the Zero Fuel Weight in klograms (float)
     88
     89    FIXME: needed for touchdown only:
     90    - ambientWindDirection: the ambient wind direction around the aircraft (float)
     91    - ambientWindSpeed: the ambient wind speed around the aircraft in knowns (float)
     92    - tdRate: the touchdown rate calculated by FSUIPC (float)
     93    """
     94   
  • src/fsuipc.py

    r3 r4  
    11# Module handling the connection to FSUIPC
     2
     3#------------------------------------------------------------------------------
     4
     5import fs
     6import const
     7import util
    28
    39import threading
    410import os
    5 import fs
    611import time
     12import calendar
     13import sys
    714
    815if os.name == "nt":
     
    1118    import pyuipc_emu as pyuipc
    1219
     20#------------------------------------------------------------------------------
     21
    1322class Handler(threading.Thread):
    1423    """The thread to handle the FSUIPC requests."""
     24    @staticmethod
     25    def _callSafe(fun):
     26        """Call the given function and swallow any exceptions."""
     27        try:
     28            return fun()
     29        except Exception, e:
     30            print >> sys.stderr, str(e)
     31            return None
     32
    1533    class Request(object):
    1634        """A simple, one-shot request."""
     
    2341           
    2442        def process(self, time):
    25             """Process the request."""
     43            """Process the request."""           
    2644            if self._forWrite:
    2745                pyuipc.write(self._data)
    28                 self._callback(self._extra)
     46                Handler._callSafe(lambda: self._callback(True, self._extra))
    2947            else:
    3048                values = pyuipc.read(self._data)
    31                 self._callback(values, self._extra)           
     49                Handler._callSafe(lambda: self._callback(values, self._extra))
    3250
    3351            return True
     52
     53        def fail(self):
     54            """Handle the failure of this request."""
     55            if self._forWrite:
     56                Handler._callSafe(lambda: self._callback(False, self._extra))
     57            else:
     58                Handler._callSafe(lambda: self._callback(None, self._extra))
    3459
    3560    class PeriodicRequest(object):
     
    6893            values = pyuipc.read(self._preparedData)
    6994
    70             self._callback(values, self._extra)
     95            Handler._callSafe(lambda: self._callback(values, self._extra))
    7196
    7297            while self._nextFire <= time:
     
    7499           
    75100            return True
     101
     102        def fail(self):
     103            """Handle the failure of this request."""
     104            pass
    76105
    77106        def __cmp__(self, other):
     
    103132
    104133        callback is a function that receives two pieces of data:
    105         - the values retrieved
     134        - the values retrieved or None on error
    106135        - the extra parameter
    107136
     
    120149        - the data to write
    121150
    122         callback is a function that receives the extra data when writing was
    123         succesful. It will called in the handler's thread!
     151        callback is a function that receives two pieces of data:
     152        - a boolean indicating if writing was successful
     153        - the extra data
     154        It will be called in the handler's thread!
    124155        """
    125156        with self._requestCondition:
    126157            self._requests.append(Handler.Request(True, data, callback, extra))
    127158            self._requestCondition.notify()
     159
     160    @staticmethod
     161    def _readWriteCallback(data, extra):
     162        """Callback for the read() and write() calls below."""
     163        extra.append(data)
     164        with extra[0] as condition:
     165            condition.notify()
     166
     167    def read(self, data):
     168        """Read the given data synchronously.
     169
     170        If a problem occurs, an exception is thrown."""
     171        with threading.Condition() as condition:
     172            extra = [condition]
     173            self._requestRead(data, self._readWriteCallback, extra)
     174            while len(extra)<2:
     175                condition.wait()
     176            if extra[1] is None:
     177                raise fs.SimulatorException("reading failed")
     178            else:
     179                return extra[1]
     180
     181    def write(self, data):
     182        """Write the given data synchronously.
     183
     184        If a problem occurs, an exception is thrown."""
     185        with threading.Condition() as condition:
     186            extra = [condition]
     187            self._requestWrite(data, self._writeCallback, extra)
     188            while len(extra)<2:
     189                condition.wait()
     190            if extra[1] is None:
     191                raise fs.SimulatorException("writing failed")
    128192
    129193    def requestPeriodicRead(self, period, data, callback, extra = None):
     
    189253                    (pyuipc.fsuipc_version, pyuipc.lib_version,
    190254                     pyuipc.fs_version)
    191                 self._connectionListener.connected(fs.TYPE_FS2K4, description)
     255                Handler._callSafe(lambda:     
     256                                  self._connectionListener.connected(const.TYPE_MSFS9,
     257                                                                     description))
    192258                return True
    193259            except Exception, e:
    194260                print "fsuipc.Handler._connect: connection failed: " + str(e)
     261                time.sleep(0.1)
    195262
    196263        return False
     
    212279        """Disconnect from the flight simulator."""
    213280        pyuipc.close()
    214         self._connectionListener.disconnected()
     281        Handler._callSafe(lambda: self._connectionListener.disconnected())
     282
     283    def _failRequests(self, request):
     284        """Fail the outstanding, single-shot requuests."""
     285        request.fail()
     286        with self._requestCondition:
     287            for request in self._requests:
     288                try:
     289                    self._requestCondition.release()
     290                    request.fail()
     291                finally:
     292                    self._requestCondition.acquire()
     293            self._requests = []       
    215294
    216295    def _processRequest(self, request, time):
     
    230309                str(e) + ") reconnecting."
    231310            self._disconnect()
     311            self._failRequests(request)
    232312            if not self._connect(): return None
    233313            else: return True
     
    254334
    255335        return self._connectionRequested
     336
     337#------------------------------------------------------------------------------
     338
     339class Simulator(object):
     340    """The simulator class representing the interface to the flight simulator
     341    via FSUIPC."""
     342    def __init__(self, connectionListener):
     343        """Construct the simulator."""
     344        self._handler = Handler(connectionListener)
     345        self._handler.start()
     346        self._aircraft = None
     347        self._aircraftName = None
     348        self._aircraftModel = None
     349        self._monitoringRequestID = None
     350
     351    def connect(self):
     352        """Initiate a connection to the simulator."""
     353        self._handler.connect()
     354
     355    def startMonitoring(self, aircraft):
     356        """Start the periodic monitoring of the aircraft and pass the resulting
     357        state to the given aircraft object periodically.
     358       
     359        The aircraft object passed must provide the following members:
     360        - type: one of the AIRCRAFT_XXX constants from const.py
     361        - modelChanged(aircraftName, modelName): called when the model handling
     362        the aircraft has changed.
     363        - handleState(aircraftState): handle the given state."""
     364        assert self._aircraft is None
     365
     366        self._aircraft = aircraft
     367        self._startMonitoring()
     368
     369    def stopMonitoring(self):
     370        """Stop the periodic monitoring of the aircraft."""
     371        self._aircraft = None
     372
     373    def disconnect(self):
     374        """Disconnect from the simulator."""
     375        self._handler.disconnect()
     376
     377    def _startMonitoring(self):
     378        """The internal call to start monitoring."""
     379        self._handler.requestRead([(0x3d00, -256)], self._monitoringStartCallback)
     380
     381    def _monitoringStartCallback(self, data, extra):
     382        """Callback for the data read when the monitoring has started.
     383       
     384        The following data items are expected:
     385        - the name of the aircraft
     386        """
     387        if self._aircraft is None:
     388            return
     389        elif data is None:
     390            self._startMonitoring()
     391        else:
     392            self._setAircraftName(data[0])           
     393
     394    def _setAircraftName(self, name):
     395        """Set the name of the aicraft and if it is different from the
     396        previous, create a new model for it.
     397       
     398        If so, also notifty the aircraft about the change."""
     399        if name==self._aircraftName:
     400            return
     401
     402        self._aircraftName = name
     403        if self._aircraftModel is None or \
     404           not self._aircraftModel.doesHandle(name):           
     405            self._setAircraftModel(AircraftModel.create(self._aircraft, name))
     406       
     407        self._aircraft.modelChanged(self._aircraftName,
     408                                    self._aircraftModel.name)
     409
     410    def _setAircraftModel(self, model):
     411        """Set a new aircraft model.
     412
     413        It will be queried for the data to monitor and the monitoring request
     414        will be replaced by a new one."""
     415        self._aircraftModel = model
     416       
     417        if self._monitoringRequestID is not None:
     418            self._handler.clearPeriodic(self._monitoringRequestID)
     419           
     420        self._monitoringRequestID = \
     421            self._handler.requestPeriodicRead(1.0,
     422                                              model.getMonitoringData(),
     423                                              self._handleMonitoringData)
     424
     425    def _handleMonitoringData(self, data, extra):
     426        """Handle the monitoring data."""
     427        if self._aircraft is None:
     428            self._handler.clearPeriodic(self._monitoringRequestID)
     429            return
     430
     431        self._setAircraftName(data[0])
     432        aircraftState = self._aircraftModel.getAircraftState(self._aircraft, data)
     433        self._aircraft.handleState(aircraftState)
     434
     435#------------------------------------------------------------------------------
     436
     437class AircraftModel(object):
     438    """Base class for the aircraft models.
     439
     440    Aircraft models handle the data arriving from FSUIPC and turn it into an
     441    object describing the aircraft's state."""
     442    monitoringData = [("aircraftName", 0x3d00, -256),
     443                      ("year", 0x0240, "H"),
     444                      ("dayOfYear", 0x023e, "H"),
     445                      ("zuluHour", 0x023b, "b"),
     446                      ("zuluMinute", 0x023c, "b"),
     447                      ("seconds", 0x023a, "b"),
     448                      ("paused", 0x0264, "H"),
     449                      ("frozen", 0x3364, "H"),
     450                      ("slew", 0x05dc, "H"),
     451                      ("overspeed", 0x036d, "b"),
     452                      ("stalled", 0x036c, "b"),
     453                      ("onTheGround", 0x0366, "H"),
     454                      ("grossWeight", 0x30c0, "f"),
     455                      ("heading", 0x0580, "d"),
     456                      ("pitch", 0x0578, "d"),
     457                      ("bank", 0x057c, "d"),
     458                      ("ias", 0x02bc, "d"),
     459                      ("vs", 0x02c8, "d"),
     460                      ("altitude", 0x0570, "l"),
     461                      ("flapsControl", 0x0bdc, "d"),
     462                      ("flapsLeft", 0x0be0, "d"),
     463                      ("flapsRight", 0x0be4, "d"),
     464                      ("lights", 0x0d0c, "H"),
     465                      ("pitot", 0x029c, "b"),
     466                      ("noseGear", 0x0bec, "d"),
     467                      ("spoilersArmed", 0x0bcc, "d"),
     468                      ("spoilers", 0x0bd0, "d")]
     469
     470    @staticmethod
     471    def create(aircraft, aircraftName):
     472        """Create the model for the given aircraft name, and notify the
     473        aircraft about it."""       
     474        return AircraftModel([0, 10, 20, 30])
     475
     476    def __init__(self, flapsNotches):
     477        """Construct the aircraft model.
     478       
     479        flapsNotches is a list of degrees of flaps that are available on the aircraft."""
     480        self._flapsNotches = flapsNotches
     481   
     482    @property
     483    def name(self):
     484        """Get the name for this aircraft model."""
     485        return "FSUIPC/Generic"
     486   
     487    def doesHandle(self, aircraftName):
     488        """Determine if the model handles the given aircraft name.
     489       
     490        This default implementation returns True."""
     491        return True
     492
     493    def addDataWithIndexMembers(self, dest, prefix, data):
     494        """Add FSUIPC data to the given array and also corresponding index
     495        member variables with the given prefix.
     496
     497        data is a list of triplets of the following items:
     498        - the name of the data item. The index member variable will have a name
     499        created by prepending the given prefix to this name.
     500        - the FSUIPC offset
     501        - the FSUIPC type
     502       
     503        The latter two items will be appended to dest."""
     504        index = len(dest)
     505        for (name, offset, type) in data:
     506            setattr(self, prefix + name, index)
     507            dest.append((offset, type))
     508            index += 1
     509           
     510    def getMonitoringData(self):
     511        """Get the data specification for monitoring.
     512       
     513        The first item should always be the aircraft name (0x3d00, -256)."""
     514        data = []
     515        self.addDataWithIndexMembers(data, "_monidx_",
     516                                     AircraftModel.monitoringData)
     517        return data
     518   
     519    def getAircraftState(self, aircraft, data):
     520        """Get an aircraft state object for the given monitoring data."""
     521        state = fs.AircraftState()
     522       
     523        timestamp = calendar.timegm(time.struct_time([data[self._monidx_year],
     524                                                      1, 1, 0, 0, 0, -1, 1, 0]))
     525        timestamp += data[self._monidx_dayOfYear] * 24 * 3600
     526        timestamp += data[self._monidx_zuluHour] * 3600
     527        timestamp += data[self._monidx_zuluMinute] * 60
     528        timestamp += data[self._monidx_seconds]       
     529        state.timestamp = timestamp
     530       
     531        state.paused = data[self._monidx_paused]!=0 or \
     532            data[self._monidx_frozen]!=0
     533        state.trickMode = data[self._monidx_slew]!=0
     534
     535        state.overspeed = data[self._monidx_overspeed]!=0
     536        state.stalled = data[self._monidx_stalled]!=0
     537        state.onTheGround = data[self._monidx_onTheGround]!=0
     538
     539        state.grossWeight = data[self._monidx_grossWeight] * util.LBSTOKG
     540       
     541        state.heading = data[self._monidx_heading]*360.0/65536.0/65536.0
     542        if state.heading<0.0: state.heading += 360.0
     543
     544        state.pitch = data[self._monidx_pitch]*360.0/65536.0/65536.0
     545        state.bank = data[self._monidx_bank]*360.0/65536.0/65536.0
     546
     547        state.ias = data[self._monidx_ias]/128.0
     548        state.vs = data[self._monidx_vs]*60.0*3.28984/256.0
     549
     550        state.altitude = data[self._monidx_altitude]*3.28084/65536.0/65536.0
     551       
     552        numNotchesM1 = len(self._flapsNotches) - 1
     553        flapsIncrement = 16383 / numNotchesM1
     554        flapsControl = data[self._monidx_flapsControl]
     555        flapsIndex = flapsControl / flapsIncrement
     556        if flapsIndex < numNotchesM1:
     557            if (flapsControl - (flapsIndex*flapsIncrement) >
     558                (flapsIndex+1)*flapsIncrement - flapsControl):
     559                flapsIndex += 1
     560        state.flapsSet = self._flapsNotches[flapsIndex]
     561       
     562        flapsLeft = data[self._monidx_flapsLeft]
     563        flapsIndex = flapsLeft / flapsIncrement
     564        state.flaps = self._flapsNotches[flapsIndex]
     565        if flapsIndex != numNotchesM1:
     566            thisNotch = flapsIndex * flapsIncrement
     567            nextNotch = thisNotch + flapsIncrement
     568           
     569            state.flaps += (self._flapsNotches[flapsIndex+1] - state.flaps) * \
     570                (flapsLeft - thisNotch) / (nextNotch - thisNotch)
     571       
     572        lights = data[self._monidx_lights]
     573       
     574        state.navLightsOn = (lights%0x01) != 0
     575        state.antiCollisionLightsOn = (lights%0x02) != 0
     576        state.landingLightsOn = (lights%0x04) != 0
     577        state.strobeLightsOn = (lights%0x10) != 0
     578       
     579        state.pitotHeatOn = data[self._monidx_pitot]!=0
     580
     581        state.gearsDown = data[self._monidx_noseGear]==16383
     582
     583        state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
     584       
     585        spoilers = data[self._monidx_spoilers]
     586        if spoilers<=4800:
     587            state.spoilersExtension = 0.0
     588        else:
     589            state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
     590       
     591        return state
Note: See TracChangeset for help on using the changeset viewer.