source: src/fsuipc.py@ 5:90eade9afbcb

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

Added some further data to those queried and reworked a bit the way the model changes are handled

File size: 23.4 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 # The basic data that should be queried all the time once we are connected
343 normalData = [ (0x3d00, -256) ]
344
345 def __init__(self, connectionListener, aircraft):
346 """Construct the simulator.
347
348 The aircraft object passed must provide the following members:
349 - type: one of the AIRCRAFT_XXX constants from const.py
350 - modelChanged(aircraftName, modelName): called when the model handling
351 the aircraft has changed.
352 - handleState(aircraftState): handle the given state."""
353 self._aircraft = aircraft
354
355 self._handler = Handler(connectionListener)
356 self._handler.start()
357
358 self._normalRequestID = None
359
360 self._monitoringRequested = False
361 self._monitoring = False
362
363 self._aircraftName = None
364 self._aircraftModel = None
365
366 def connect(self):
367 """Initiate a connection to the simulator."""
368 self._handler.connect()
369 self._startDefaultNormal()
370
371 def startMonitoring(self):
372 """Start the periodic monitoring of the aircraft and pass the resulting
373 state to the aircraft object periodically."""
374 assert not self._monitoringRequested
375 self._monitoringRequested = True
376
377 def stopMonitoring(self):
378 """Stop the periodic monitoring of the aircraft."""
379 assert self._monitoringRequested
380 self._monitoringRequested = False
381
382 def disconnect(self):
383 """Disconnect from the simulator."""
384 assert not self._monitoringRequested
385
386 self._stopNormal()
387 self._handler.disconnect()
388
389 def _startDefaultNormal(self):
390 """Start the default normal periodic request."""
391 assert self._normalRequestID is None
392 self._normalRequestID = self._handler.requestPeriodicRead(1.0,
393 Simulator.normalData,
394 self._handleNormal)
395
396 def _stopNormal(self):
397 """Stop the normal period request."""
398 assert self._normalRequestID is not None
399 self._handler.clearPeriodic(self._normalRequestID)
400 self._normalRequestID = None
401
402 def _handleNormal(self, data, extra):
403 """Handle the reply to the normal request.
404
405 At the beginning the result consists the data for normalData. When
406 monitoring is started, it contains the result also for the
407 aircraft-specific values.
408 """
409 self._setAircraftName(data[0])
410 if self._monitoringRequested and not self._monitoring:
411 self._monitoring = True
412 self._stopNormal()
413 self._startMonitoring()
414 elif self._monitoring and not self._monitoringRequested:
415 self._monitoring = False
416 self._stopNormal()
417 self._startDefaultNormal()
418 elif self._monitoring and self._aircraftModel is not None:
419 aircraftState = self._aircraftModel.getAircraftState(self._aircraft, data)
420 self._aircraft.handleState(aircraftState)
421
422 def _setAircraftName(self, name):
423 """Set the name of the aicraft and if it is different from the
424 previous, create a new model for it.
425
426 If so, also notifty the aircraft about the change."""
427 if name==self._aircraftName:
428 return
429
430 self._aircraftName = name
431 if self._aircraftModel is None or \
432 not self._aircraftModel.doesHandle(name):
433 self._setAircraftModel(AircraftModel.create(self._aircraft, name))
434
435 self._aircraft.modelChanged(self._aircraftName,
436 self._aircraftModel.name)
437
438 def _setAircraftModel(self, model):
439 """Set a new aircraft model.
440
441 It will be queried for the data to monitor and the monitoring request
442 will be replaced by a new one."""
443 self._aircraftModel = model
444
445 if self._monitoring:
446 self._handler.clearPeriodic(self._normalRequestID)
447 self._startMonitoring()
448
449 def _startMonitoring(self):
450 """Start monitoring with the current aircraft model."""
451 assert self._monitoring
452
453 data = Simulator.normalData[:]
454 self._aircraftModel.addMonitoringData(data)
455
456 self._normalRequestID = \
457 self._handler.requestPeriodicRead(1.0, data,
458 self._handleNormal)
459
460#------------------------------------------------------------------------------
461
462class AircraftModel(object):
463 """Base class for the aircraft models.
464
465 Aircraft models handle the data arriving from FSUIPC and turn it into an
466 object describing the aircraft's state."""
467 monitoringData = [("year", 0x0240, "H"),
468 ("dayOfYear", 0x023e, "H"),
469 ("zuluHour", 0x023b, "b"),
470 ("zuluMinute", 0x023c, "b"),
471 ("seconds", 0x023a, "b"),
472 ("paused", 0x0264, "H"),
473 ("frozen", 0x3364, "H"),
474 ("replay", 0x0628, "d"),
475 ("slew", 0x05dc, "H"),
476 ("overspeed", 0x036d, "b"),
477 ("stalled", 0x036c, "b"),
478 ("onTheGround", 0x0366, "H"),
479 ("grossWeight", 0x30c0, "f"),
480 ("heading", 0x0580, "d"),
481 ("pitch", 0x0578, "d"),
482 ("bank", 0x057c, "d"),
483 ("ias", 0x02bc, "d"),
484 ("groundSpeed", 0x02b4, "d"),
485 ("vs", 0x02c8, "d"),
486 ("altitude", 0x0570, "l"),
487 ("gLoad", 0x11ba, "H"),
488 ("flapsControl", 0x0bdc, "d"),
489 ("flapsLeft", 0x0be0, "d"),
490 ("flapsRight", 0x0be4, "d"),
491 ("lights", 0x0d0c, "H"),
492 ("pitot", 0x029c, "b"),
493 ("noseGear", 0x0bec, "d"),
494 ("spoilersArmed", 0x0bcc, "d"),
495 ("spoilers", 0x0bd0, "d"),
496 ("altimeter", 0x0330, "H"),
497 ("nav1", 0x0350, "H"),
498 ("nav2", 0x0352, "H")]
499
500 @staticmethod
501 def create(aircraft, aircraftName):
502 """Create the model for the given aircraft name, and notify the
503 aircraft about it."""
504 return AircraftModel([0, 10, 20, 30])
505
506 @staticmethod
507 def convertFrequency(data):
508 """Convert the given frequency data to a string."""
509 frequency = ""
510 for i in range(0, 4):
511 digit = chr(ord('0') + (data&0x0f))
512 data >>= 4
513 frequency = digit + frequency
514 if i==1:
515 frequency = "." + frequency
516 return "1" + frequency
517
518 def __init__(self, flapsNotches):
519 """Construct the aircraft model.
520
521 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
522 self._flapsNotches = flapsNotches
523
524 @property
525 def name(self):
526 """Get the name for this aircraft model."""
527 return "FSUIPC/Generic"
528
529 def doesHandle(self, aircraftName):
530 """Determine if the model handles the given aircraft name.
531
532 This default implementation returns True."""
533 return True
534
535 def addDataWithIndexMembers(self, dest, prefix, data):
536 """Add FSUIPC data to the given array and also corresponding index
537 member variables with the given prefix.
538
539 data is a list of triplets of the following items:
540 - the name of the data item. The index member variable will have a name
541 created by prepending the given prefix to this name.
542 - the FSUIPC offset
543 - the FSUIPC type
544
545 The latter two items will be appended to dest."""
546 index = len(dest)
547 for (name, offset, type) in data:
548 setattr(self, prefix + name, index)
549 dest.append((offset, type))
550 index += 1
551
552 def addMonitoringData(self, data):
553 """Get the data specification for monitoring.
554
555 Add the model-specific monitoring data to the given array."""
556 self.addDataWithIndexMembers(data, "_monidx_",
557 AircraftModel.monitoringData)
558
559 def getAircraftState(self, aircraft, data):
560 """Get an aircraft state object for the given monitoring data."""
561 state = fs.AircraftState()
562
563 timestamp = calendar.timegm(time.struct_time([data[self._monidx_year],
564 1, 1, 0, 0, 0, -1, 1, 0]))
565 timestamp += data[self._monidx_dayOfYear] * 24 * 3600
566 timestamp += data[self._monidx_zuluHour] * 3600
567 timestamp += data[self._monidx_zuluMinute] * 60
568 timestamp += data[self._monidx_seconds]
569 state.timestamp = timestamp
570
571 state.paused = data[self._monidx_paused]!=0 or \
572 data[self._monidx_frozen]!=0 or \
573 data[self._monidx_replay]!=0
574 state.trickMode = data[self._monidx_slew]!=0
575
576 state.overspeed = data[self._monidx_overspeed]!=0
577 state.stalled = data[self._monidx_stalled]!=0
578 state.onTheGround = data[self._monidx_onTheGround]!=0
579
580 state.grossWeight = data[self._monidx_grossWeight] * util.LBSTOKG
581
582 state.heading = data[self._monidx_heading]*360.0/65536.0/65536.0
583 if state.heading<0.0: state.heading += 360.0
584
585 state.pitch = data[self._monidx_pitch]*360.0/65536.0/65536.0
586 state.bank = data[self._monidx_bank]*360.0/65536.0/65536.0
587
588 state.ias = data[self._monidx_ias]/128.0
589 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
590 state.vs = data[self._monidx_vs]*60.0*3.28984/256.0
591
592 state.altitude = data[self._monidx_altitude]*3.28084/65536.0/65536.0
593
594 state.gLoad = data[self._monidx_gLoad] / 625.0
595
596 numNotchesM1 = len(self._flapsNotches) - 1
597 flapsIncrement = 16383 / numNotchesM1
598 flapsControl = data[self._monidx_flapsControl]
599 flapsIndex = flapsControl / flapsIncrement
600 if flapsIndex < numNotchesM1:
601 if (flapsControl - (flapsIndex*flapsIncrement) >
602 (flapsIndex+1)*flapsIncrement - flapsControl):
603 flapsIndex += 1
604 state.flapsSet = self._flapsNotches[flapsIndex]
605
606 flapsLeft = data[self._monidx_flapsLeft]
607 flapsIndex = flapsLeft / flapsIncrement
608 state.flaps = self._flapsNotches[flapsIndex]
609 if flapsIndex != numNotchesM1:
610 thisNotch = flapsIndex * flapsIncrement
611 nextNotch = thisNotch + flapsIncrement
612
613 state.flaps += (self._flapsNotches[flapsIndex+1] - state.flaps) * \
614 (flapsLeft - thisNotch) / (nextNotch - thisNotch)
615
616 lights = data[self._monidx_lights]
617
618 state.navLightsOn = (lights&0x01) != 0
619 state.antiCollisionLightsOn = (lights&0x02) != 0
620 state.landingLightsOn = (lights&0x04) != 0
621 state.strobeLightsOn = (lights&0x10) != 0
622
623 state.pitotHeatOn = data[self._monidx_pitot]!=0
624
625 state.gearsDown = data[self._monidx_noseGear]==16383
626
627 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
628
629 spoilers = data[self._monidx_spoilers]
630 if spoilers<=4800:
631 state.spoilersExtension = 0.0
632 else:
633 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
634
635 state.altimeter = data[self._monidx_altimeter] / 16.0
636
637 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
638 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
639
640 return state
641
642#------------------------------------------------------------------------------
643
Note: See TracBrowser for help on using the repository browser.