source: src/fsuipc.py@ 4:bcc93ecb8cb6

Last change on this file since 4:bcc93ecb8cb6 was 4:bcc93ecb8cb6, checked in by István Váradi <ivaradi@…>, 12 years ago

The aircraft model framework is basically implemented

File size: 21.3 KB
Line 
1# Module handling the connection to FSUIPC
2
3#------------------------------------------------------------------------------
4
5import fs
6import const
7import util
8
9import threading
10import os
11import time
12import calendar
13import sys
14
15if os.name == "nt":
16 import pyuipc
17else:
18 import pyuipc_emu as pyuipc
19
20#------------------------------------------------------------------------------
21
22class Handler(threading.Thread):
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
33 class Request(object):
34 """A simple, one-shot request."""
35 def __init__(self, forWrite, data, callback, extra):
36 """Construct the request."""
37 self._forWrite = forWrite
38 self._data = data
39 self._callback = callback
40 self._extra = extra
41
42 def process(self, time):
43 """Process the request."""
44 if self._forWrite:
45 pyuipc.write(self._data)
46 Handler._callSafe(lambda: self._callback(True, self._extra))
47 else:
48 values = pyuipc.read(self._data)
49 Handler._callSafe(lambda: self._callback(values, self._extra))
50
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))
59
60 class PeriodicRequest(object):
61 """A periodic request."""
62 def __init__(self, id, period, data, callback, extra):
63 """Construct the periodic request."""
64 self._id = id
65 self._period = period
66 self._nextFire = time.time() + period
67 self._data = data
68 self._preparedData = None
69 self._callback = callback
70 self._extra = extra
71
72 @property
73 def id(self):
74 """Get the ID of this periodic request."""
75 return self._id
76
77 @property
78 def nextFire(self):
79 """Get the next firing time."""
80 return self._nextFire
81
82 def process(self, time):
83 """Check if this request should be executed, and if so, do so.
84
85 Return a boolean indicating if the request was executed."""
86 if time < self._nextFire:
87 return False
88
89 if self._preparedData is None:
90 self._preparedData = pyuipc.prepare_data(self._data)
91 self._data = None
92
93 values = pyuipc.read(self._preparedData)
94
95 Handler._callSafe(lambda: self._callback(values, self._extra))
96
97 while self._nextFire <= time:
98 self._nextFire += self._period
99
100 return True
101
102 def fail(self):
103 """Handle the failure of this request."""
104 pass
105
106 def __cmp__(self, other):
107 """Compare two periodic requests. They are ordered by their next
108 firing times."""
109 return cmp(self._nextFire, other._nextFire)
110
111 def __init__(self, connectionListener):
112 """Construct the handler with the given connection listener."""
113 threading.Thread.__init__(self)
114
115 self._connectionListener = connectionListener
116
117 self._requestCondition = threading.Condition()
118 self._connectionRequested = False
119
120 self._requests = []
121 self._nextPeriodicID = 1
122 self._periodicRequests = []
123
124 self.daemon = True
125
126 def requestRead(self, data, callback, extra = None):
127 """Request the reading of some data.
128
129 data is a list of tuples of the following items:
130 - the offset of the data as an integer
131 - the type letter of the data as a string
132
133 callback is a function that receives two pieces of data:
134 - the values retrieved or None on error
135 - the extra parameter
136
137 It will be called in the handler's thread!
138 """
139 with self._requestCondition:
140 self._requests.append(Handler.Request(False, data, callback, extra))
141 self._requestCondition.notify()
142
143 def requestWrite(self, data, callback, extra = None):
144 """Request the writing of some data.
145
146 data is a list of tuples of the following items:
147 - the offset of the data as an integer
148 - the type letter of the data as a string
149 - the data to write
150
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!
155 """
156 with self._requestCondition:
157 self._requests.append(Handler.Request(True, data, callback, extra))
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")
192
193 def requestPeriodicRead(self, period, data, callback, extra = None):
194 """Request a periodic read of data.
195
196 period is a floating point number with the period in seconds.
197
198 This function returns an identifier which can be used to cancel the
199 request."""
200 with self._requestCondition:
201 id = self._nextPeriodicID
202 self._nextPeriodicID += 1
203 request = Handler.PeriodicRequest(id, period, data, callback, extra)
204 self._periodicRequests.append(request)
205 self._requestCondition.notify()
206 return id
207
208 def clearPeriodic(self, id):
209 """Clear the periodic request with the given ID."""
210 with self._requestCondition:
211 for i in range(0, len(self._periodicRequests)):
212 if self._periodicRequests[i].id==id:
213 del self._periodicRequests[i]
214 return True
215 return False
216
217 def connect(self):
218 """Initiate the connection to the flight simulator."""
219 with self._requestCondition:
220 if not self._connectionRequested:
221 self._connectionRequested = True
222 self._requestCondition.notify()
223
224 def disconnect(self):
225 """Disconnect from the flight simulator."""
226 with self._requestCondition:
227 if self._connectionRequested:
228 self._connectionRequested = False
229 self._requestCondition.notify()
230
231 def run(self):
232 """Perform the operation of the thread."""
233 while True:
234 self._waitConnectionRequest()
235
236 if self._connect():
237 self._handleConnection()
238
239 self._disconnect()
240
241 def _waitConnectionRequest(self):
242 """Wait for a connection request to arrive."""
243 with self._requestCondition:
244 while not self._connectionRequested:
245 self._requestCondition.wait()
246
247 def _connect(self):
248 """Try to connect to the flight simulator via FSUIPC"""
249 while self._connectionRequested:
250 try:
251 pyuipc.open(pyuipc.SIM_FS2K4)
252 description = "(FSUIPC version: 0x%04x, library version: 0x%04x, FS version: %d)" % \
253 (pyuipc.fsuipc_version, pyuipc.lib_version,
254 pyuipc.fs_version)
255 Handler._callSafe(lambda:
256 self._connectionListener.connected(const.TYPE_MSFS9,
257 description))
258 return True
259 except Exception, e:
260 print "fsuipc.Handler._connect: connection failed: " + str(e)
261 time.sleep(0.1)
262
263 return False
264
265 def _handleConnection(self):
266 """Handle a living connection."""
267 with self._requestCondition:
268 while self._connectionRequested:
269 if not self._processRequests():
270 return
271 timeout = None
272 if self._periodicRequests:
273 self._periodicRequests.sort()
274 timeout = self._periodicRequests[0].nextFire - time.time()
275 if timeout is None or timeout > 0.0:
276 self._requestCondition.wait(timeout)
277
278 def _disconnect(self):
279 """Disconnect from the flight simulator."""
280 pyuipc.close()
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 = []
294
295 def _processRequest(self, request, time):
296 """Process the given request.
297
298 If an exception occurs, we try to reconnect.
299
300 Returns what the request's process() function returned or None if
301 reconnection failed."""
302
303 self._requestCondition.release()
304
305 try:
306 return request.process(time)
307 except Exception as e:
308 print "fsuipc.Handler._processRequest: FSUIPC connection failed (" + \
309 str(e) + ") reconnecting."
310 self._disconnect()
311 self._failRequests(request)
312 if not self._connect(): return None
313 else: return True
314 finally:
315 self._requestCondition.acquire()
316
317 def _processRequests(self):
318 """Process any pending requests.
319
320 Will be called with the request lock held."""
321 while self._connectionRequested and self._periodicRequests:
322 self._periodicRequests.sort()
323 request = self._periodicRequests[0]
324 result = self._processRequest(request, time.time())
325 if result is None: return False
326 elif not result: break
327
328 while self._connectionRequested and self._requests:
329 request = self._requests[0]
330 del self._requests[0]
331
332 if self._processRequest(request, None) is None:
333 return False
334
335 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 TracBrowser for help on using the repository browser.