Changeset 4:bcc93ecb8cb6
- Timestamp:
- 01/29/12 14:00:30 (13 years ago)
- Branch:
- default
- Phase:
- public
- Location:
- src
- Files:
-
- 4 added
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
src/fs.py
r3 r4 1 1 # Module for generic flight-simulator interfaces 2 2 3 # Flight simulator type: MS Flight Simulator 2004 4 TYPE_FS2K4 = 1 3 #------------------------------------------------------------------------------- 5 4 6 # Flight simulator type: MS Flight Simulator X 7 TYPE_FSX = 2 5 import const 8 6 9 # Flight simulator type: X-Plane 9 10 TYPE_XPLANE9 = 3 7 #------------------------------------------------------------------------------- 11 8 12 # Flight simulator type: X-Plane 10 13 TYPE_XPLANE10 = 4 14 15 class ConnectionListener: 9 class ConnectionListener(object): 16 10 """Base class for listeners on connections to the flight simulator.""" 17 11 def connected(self, fsType, descriptor): … … 24 18 print "fs.ConnectionListener.disconnected" 25 19 20 #------------------------------------------------------------------------------- 21 22 class SimulatorException(Exception): 23 """Exception thrown by the simulator interface for communication failure.""" 24 25 #------------------------------------------------------------------------------- 26 27 def 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 40 class 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 1 1 # Module handling the connection to FSUIPC 2 3 #------------------------------------------------------------------------------ 4 5 import fs 6 import const 7 import util 2 8 3 9 import threading 4 10 import os 5 import fs6 11 import time 12 import calendar 13 import sys 7 14 8 15 if os.name == "nt": … … 11 18 import pyuipc_emu as pyuipc 12 19 20 #------------------------------------------------------------------------------ 21 13 22 class Handler(threading.Thread): 14 23 """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 15 33 class Request(object): 16 34 """A simple, one-shot request.""" … … 23 41 24 42 def process(self, time): 25 """Process the request.""" 43 """Process the request.""" 26 44 if self._forWrite: 27 45 pyuipc.write(self._data) 28 self._callback(self._extra)46 Handler._callSafe(lambda: self._callback(True, self._extra)) 29 47 else: 30 48 values = pyuipc.read(self._data) 31 self._callback(values, self._extra)49 Handler._callSafe(lambda: self._callback(values, self._extra)) 32 50 33 51 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)) 34 59 35 60 class PeriodicRequest(object): … … 68 93 values = pyuipc.read(self._preparedData) 69 94 70 self._callback(values, self._extra)95 Handler._callSafe(lambda: self._callback(values, self._extra)) 71 96 72 97 while self._nextFire <= time: … … 74 99 75 100 return True 101 102 def fail(self): 103 """Handle the failure of this request.""" 104 pass 76 105 77 106 def __cmp__(self, other): … … 103 132 104 133 callback is a function that receives two pieces of data: 105 - the values retrieved 134 - the values retrieved or None on error 106 135 - the extra parameter 107 136 … … 120 149 - the data to write 121 150 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! 124 155 """ 125 156 with self._requestCondition: 126 157 self._requests.append(Handler.Request(True, data, callback, extra)) 127 158 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") 128 192 129 193 def requestPeriodicRead(self, period, data, callback, extra = None): … … 189 253 (pyuipc.fsuipc_version, pyuipc.lib_version, 190 254 pyuipc.fs_version) 191 self._connectionListener.connected(fs.TYPE_FS2K4, description) 255 Handler._callSafe(lambda: 256 self._connectionListener.connected(const.TYPE_MSFS9, 257 description)) 192 258 return True 193 259 except Exception, e: 194 260 print "fsuipc.Handler._connect: connection failed: " + str(e) 261 time.sleep(0.1) 195 262 196 263 return False … … 212 279 """Disconnect from the flight simulator.""" 213 280 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 = [] 215 294 216 295 def _processRequest(self, request, time): … … 230 309 str(e) + ") reconnecting." 231 310 self._disconnect() 311 self._failRequests(request) 232 312 if not self._connect(): return None 233 313 else: return True … … 254 334 255 335 return self._connectionRequested 336 337 #------------------------------------------------------------------------------ 338 339 class 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 437 class 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.