source: src/mlx/pyuipc_sim.py@ 53:46b91acfddfc

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

A very basic server and client works for the PyUIPC simulator

File size: 20.5 KB
Line 
1# Simulator for the pyuipc module
2#------------------------------------------------------------------------------
3
4import const
5
6import cmd
7import threading
8import socket
9import time
10import sys
11import struct
12import cPickle
13
14#------------------------------------------------------------------------------
15
16# Version constants
17SIM_ANY=0
18SIM_FS98=1
19SIM_FS2K=2
20SIM_CFS2=3
21SIM_CFS1=4
22SIM_FLY=5
23SIM_FS2K2=6
24SIM_FS2K4=7
25
26#------------------------------------------------------------------------------
27
28# Error constants
29ERR_OK=0
30ERR_OPEN=1
31ERR_NOFS=2
32ERR_REGMSG=3
33ERR_ATOM=4
34ERR_MAP=5
35ERR_VIEW=6
36ERR_VERSION=7
37ERR_WRONGFS=8
38ERR_NOTOPEN=9
39ERR_NODATA=10
40ERR_TIMEOUT=11
41ERR_SENDMSG=12
42ERR_DATA=13
43ERR_RUNNING=14
44ERR_SIZE=15
45
46#------------------------------------------------------------------------------
47
48# The version of FSUIPC
49fsuipc_version=0x0401
50lib_version=0x0302
51fs_version=SIM_FS2K4
52
53#------------------------------------------------------------------------------
54
55class FSUIPCException(Exception):
56 """FSUIPC exception class.
57
58 It contains a member variable named errorCode. The string is a text
59 describing the error."""
60
61 errors=["OK",
62 "Attempt to Open when already Open",
63 "Cannot link to FSUIPC or WideClient",
64 "Failed to Register common message with Windows",
65 "Failed to create Atom for mapping filename",
66 "Failed to create a file mapping object",
67 "Failed to open a view to the file map",
68 "Incorrect version of FSUIPC, or not FSUIPC",
69 "Sim is not version requested",
70 "Call cannot execute, link not Open",
71 "Call cannot execute: no requests accumulated",
72 "IPC timed out all retries",
73 "IPC sendmessage failed all retries",
74 "IPC request contains bad data",
75 "Maybe running on WideClient, but FS not running on Server, or wrong FSUIPC",
76 "Read or Write request cannot be added, memory for Process is full"]
77
78 def __init__(self, errorCode):
79 """
80 Construct the exception
81 """
82 if errorCode<len(self.errors):
83 self.errorString = self.errors[errorCode]
84 else:
85 self.errorString = "Unknown error"
86 Exception.__init__(self, self.errorString)
87 self.errorCode = errorCode
88
89 def __str__(self):
90 """
91 Convert the excption to string
92 """
93 return "FSUIPC error: %d (%s)" % (self.errorCode, self.errorString)
94
95#------------------------------------------------------------------------------
96
97class Values(object):
98 """The values that can be read from 'FSUIPC'."""
99 # Fuel data index: centre tank
100 FUEL_CENTRE = 0
101
102 # Fuel data index: left main tank
103 FUEL_LEFT = 1
104
105 # Fuel data index: right main tank
106 FUEL_RIGHT = 2
107
108 # Fuel data index: left aux tank
109 FUEL_LEFT_AUX = 3
110
111 # Fuel data index: right aux tank
112 FUEL_RIGHT_AUX = 4
113
114 # Fuel data index: left tip tank
115 FUEL_LEFT_TIP = 5
116
117 # Fuel data index: right tip tank
118 FUEL_RIGHT_AUX = 6
119
120 # Fuel data index: external 1 tank
121 FUEL_EXTERNAL_1 = 7
122
123 # Fuel data index: external 2 tank
124 FUEL_EXTERNAL_2 = 8
125
126 # Fuel data index: centre 2 tank
127 FUEL_CENTRE_2 = 9
128
129 # The number of fuel tank entries
130 NUM_FUEL = FUEL_CENTRE_2 + 1
131
132 # Engine index: engine #1
133 ENGINE_1 = 0
134
135 # Engine index: engine #2
136 ENGINE_2 = 1
137
138 # Engine index: engine #3
139 ENGINE_3 = 2
140
141 @staticmethod
142 def _convertFrequency(frequency):
143 """Convert the given frequency into BCD."""
144 return Values._convertBCD(int(frequency*100.0))
145
146 @staticmethod
147 def _convertBCD(value):
148 """Convert the given value into BCD format."""
149 bcd = (value/1000) % 10
150 bcd <<= 4
151 bcd |= (value/100) % 10
152 bcd <<= 4
153 bcd |= (value/10) % 10
154 bcd <<= 4
155 bcd |= value % 10
156 return bcd
157
158 def __init__(self):
159 """Construct the values with defaults."""
160 self._timeOffset = 0
161 self.airPath = "C:\\Program Files\\Microsoft Games\\" \
162 "FS9\\Aircraft\\Cessna\\cessna172.air"
163 self.aircraftName = "Cessna 172SP"
164 self.flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40]
165 self.fuelCapacities = [10000.0, 5000.0, 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
166
167 self.paused = False
168 self.frozen = False
169 self.replay = False
170 self.slew = False
171 self.overspeed = False
172 self.stalled = False
173 self.onTheGround = True
174
175 self.zfw = 50000.0
176
177 self.fuelWeights = [0.0, 3000.0, 3000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
178 # FIXME: check for realistic values
179 self.fuelWeight = 3.5
180
181 self.heading = 220.0
182 self.pitch = 0.0
183 self.bank = 0.0
184
185 self.ias = 0.0
186 self.vs = 0.0
187
188 self.radioAltitude = None
189 self.altitude = 513.0
190
191 self.gLoad = 1.0
192
193 self.flapsControl = 0.0
194 self.flaps = 0.0
195
196 self.navLightsOn = True
197 self.antiCollisionLightsOn = False
198 self.landingLightsOn = False
199 self.strobeLightsOn = False
200
201 self.pitot = False
202 self.parking = True
203
204 self.noseGear = 1.0
205
206 self.spoilersArmed = False
207 self.spoilers = 0.0
208
209 self.altimeter = 1013.0
210
211 self.nav1 = 117.3
212 self.nav2 = 109.5
213 self.squawk = 2200
214
215 self.windSpeed = 8.0
216 self.windDirection = 300.0
217
218 self.n1 = [0.0, 0.0, 0.0]
219 self.throttles = [0.0, 0.0, 0.0]
220
221 def read(self, offset):
222 """Read the value at the given offset."""
223 try:
224 return self._read(offset)
225 except Exception, e:
226 print "failed to read offset %04x: %s" % (offset, str(e))
227 raise FSUIPCException(ERR_DATA)
228
229 def _read(self, offset):
230 """Read the value at the given offset."""
231 if offset==0x023a: # Second of time
232 return self._readUTC().tm_sec
233 elif offset==0x023b: # Hour of Zulu time
234 return self._readUTC().tm_hour
235 elif offset==0x023c: # Minute of Zulu time
236 return self._readUTC().tm_min
237 elif offset==0x023e: # Day number on year
238 return self._readUTC().tm_yday
239 elif offset==0x0240: # Year in FS
240 return self._readUTC().tm_year
241 elif offset==0x0264: # Paused
242 return 1 if self.paused else 0
243 elif offset==0x029c: # Pitot
244 return 1 if self.pitot else 0
245 elif offset==0x02b4: # Ground speed
246 # FIXME: calculate TAS first, then from the heading and
247 # wind the GS
248 return int(self.ias * 65536.0 * 1852.0 / 3600.0)
249 elif offset==0x02bc: # IAS
250 return int(self.ias * 128.0)
251 elif offset==0x02c8: # VS
252 return int(self.vs * const.FEETTOMETRES * 256.0 / 60.0)
253 elif offset==0x0330: # Altimeter
254 return int(self.altimeter * 16.0)
255 elif offset==0x0350: # NAV1
256 return Values._convertFrequency(self.nav1)
257 elif offset==0x0352: # NAV2
258 return Values._convertFrequency(self.nav2)
259 elif offset==0x0354: # Squawk
260 return Values._convertBCD(self.squawk)
261 elif offset==0x0366: # Stalled
262 return 1 if self.stalled else 0
263 elif offset==0x036c: # Stalled
264 return 1 if self.stalled else 0
265 elif offset==0x036d: # Overspeed
266 return 1 if self.overspeed else 0
267 elif offset==0x0570: # Altitude
268 return long(self.altitude * const.FEETTOMETRES * 65536.0 * 65536.0)
269 elif offset==0x0578: # Pitch
270 return int(self.pitch * 65536.0 * 65536.0 / 360.0)
271 elif offset==0x057c: # Bank
272 return int(self.bank * 65536.0 * 65536.0 / 360.0)
273 elif offset==0x0580: # Heading
274 return int(self.heading * 65536.0 * 65536.0 / 360.0)
275 elif offset==0x05dc: # Slew
276 return 1 if self.slew else 0
277 elif offset==0x0628: # Replay
278 return 1 if self.replay else 0
279 elif offset==0x088c: # Engine #1 throttle
280 return self._getThrottle(self.ENGINE_1)
281 elif offset==0x0924: # Engine #2 throttle
282 return self._getThrottle(self.ENGINE_2)
283 elif offset==0x09bc: # Engine #3 throttle
284 return self._getThrottle(self.ENGINE_3)
285 elif offset==0x0af4: # Fuel weight
286 return int(self.fuelWeight * 256.0)
287 elif offset==0x0b74: # Centre tank level
288 return self._getFuelLevel(self.FUEL_CENTRE)
289 elif offset==0x0b78: # Centre tank capacity
290 return self._getFuelCapacity(self.FUEL_CENTRE)
291 elif offset==0x0b7c: # Left tank level
292 return self._getFuelLevel(self.FUEL_LEFT)
293 elif offset==0x0b80: # Left tank capacity
294 return self._getFuelCapacity(self.FUEL_LEFT)
295 elif offset==0x0b84: # Left aux tank level
296 return self._getFuelLevel(self.FUEL_LEFT_AUX)
297 elif offset==0x0b88: # Left aux tank capacity
298 return self._getFuelCapacity(self.FUEL_LEFT_AUX)
299 elif offset==0x0b8c: # Left tip tank level
300 return self._getFuelLevel(self.FUEL_LEFT_TIP)
301 elif offset==0x0b90: # Left tip tank capacity
302 return self._getFuelCapacity(self.FUEL_LEFT_TIP)
303 elif offset==0x0b94: # Right aux tank level
304 return self._getFuelLevel(self.FUEL_RIGHT)
305 elif offset==0x0b98: # Right aux tank capacity
306 return self._getFuelCapacity(self.FUEL_RIGHT)
307 elif offset==0x0b9c: # Right tank level
308 return self._getFuelLevel(self.FUEL_RIGHT_AUX)
309 elif offset==0x0ba0: # Right tank capacity
310 return self._getFuelCapacity(self.FUEL_RIGHT_AUX)
311 elif offset==0x0ba4: # Right tip tank level
312 return self._getFuelLevel(self.FUEL_RIGHT_TIP)
313 elif offset==0x0ba8: # Right tip tank capacity
314 return self._getFuelCapacity(self.FUEL_RIGHT_TIP)
315 elif offset==0x0bc8: # Parking
316 return 1 if self.parking else 0
317 elif offset==0x0bcc: # Spoilers armed
318 return 1 if self.spoilersArmed else 0
319 elif offset==0x0bd0: # Spoilers
320 return 0 if self.spoilers == 0 \
321 else int(self.spoilers * (16383 - 4800) + 4800)
322 elif offset==0x0bdc: # Flaps control
323 numNotchesM1 = len(self.flapsNotches) - 1
324 flapsIncrement = 16383.0 / numNotchesM1
325 index = 0
326 while index<numNotchesM1 and \
327 self.flapsControl<self.flapsNotches[index]:
328 index += 1
329
330 if index==numNotchesM1:
331 return 16383
332 else:
333 return int((self.flapsControl-self.flapsNotches[index]) * \
334 flapsIncrement / \
335 (self.flapsNotches[index+1] - self.flapsNotches[index]))
336 elif offset==0x0be0 or offset==0x0be4: # Flaps left and right
337 return self.flaps * 16383.0 / self.flapsNotches[-1]
338 elif offset==0x0bec: # Nose gear
339 return int(self.noseGear * 16383.0)
340 elif offset==0x0d0c: # Lights
341 lights = 0
342 if self.navLightsOn: lights |= 0x01
343 if self.antiCollisionLightsOn: lights |= 0x02
344 if self.landingLightsOn: lights |= 0x04
345 if self.strobeLightsOn: lights |= 0x10
346 return lights
347 elif offset==0x0e90: # Wind speed
348 return int(self.windSpeed)
349 elif offset==0x0e92: # Wind direction
350 return int(self.windDirection * 65536.0 / 360.0)
351 elif offset==0x11ba: # G-Load
352 return int(self.gLoad * 625.0)
353 elif offset==0x11c6: # Mach
354 # FIXME: calculate from IAS, altitude and QNH
355 return int(self.ias * 0.05 * 20480.)
356 elif offset==0x1244: # Centre 2 tank level
357 return self._getFuelLevel(self.FUEL_CENTRE_2)
358 elif offset==0x1248: # Centre 2 tank capacity
359 return self._getFuelCapacity(self.FUEL_CENTRE_2)
360 elif offset==0x1254: # External 1 tank level
361 return self._getFuelLevel(self.FUEL_EXTERNAL_1)
362 elif offset==0x1258: # External 1 tank capacity
363 return self._getFuelCapacity(self.FUEL_EXTERNAL_1)
364 elif offset==0x125c: # External 2 tank level
365 return self._getFuelLevel(self.FUEL_EXTERNAL_2)
366 elif offset==0x1260: # External 2 tank capacity
367 return self._getFuelCapacity(self.FUEL_EXTERNAL_2)
368 elif offset==0x2000: # Engine #1 N1
369 return self.n1[self.ENGINE_1]
370 elif offset==0x2100: # Engine #2 N1
371 return self.n1[self.ENGINE_2]
372 elif offset==0x2200: # Engine #3 N1
373 return self.n1[self.ENGINE_3]
374 elif offset==0x30c0: # Grossweight
375 return int((self.zfw + sum(self.fuelWeights)) * const.KGSTOLB)
376 elif offset==0x31e4: # Radio altitude
377 # FIXME: if self.radioAltitude is None, calculate from the
378 # altitude with some, perhaps random, ground altitude
379 # value
380 radioAltitude = (self.altitude - 517) \
381 if self.radioAltitude is None else self.radioAltitude
382 return (radioAltitude * const.FEETTOMETRES * 65536.0)
383 elif offset==0x3364: # Frozen
384 return 1 if self.frozen else 0
385 elif offset==0x3bfc: # ZFW
386 return int(self.zfw * 256.0 * const.KGSTOLB)
387 elif offset==0x3c00: # Path of the current AIR file
388 return self.airPath
389 elif offset==0x3d00: # Name of the current aircraft
390 return self.aircraftName
391 else:
392 print "Unhandled offset: %04x" % (offset,)
393 raise FSUIPCException(ERR_DATA)
394
395 def _readUTC(self):
396 """Read the UTC time.
397
398 The current offset is added to it."""
399 return time.gmtime(time.time() + self._timeOffset)
400
401 def _getFuelLevel(self, index):
402 """Get the fuel level for the fuel tank with the given
403 index."""
404 # FIXME: check if the constants are correct
405 return 0 if self.fuelCapacities[index]==0.0 else \
406 int(self.fuelWeights[index] * 65536.0 / self.fuelCapacities[index])
407
408 def _getFuelCapacity(self, index):
409 """Get the capacity of the fuel tank with the given index."""
410 # FIXME: check if the constants are correct
411 return int(self.fuelCapacities[index] * const.KGSTOLB * 128.0 /
412 self.fuelWeight)
413
414 def _getThrottle(self, index):
415 """Get the throttle value for the given index."""
416 return int(self.throttles[index] * 16383.0)
417
418#------------------------------------------------------------------------------
419
420values = Values()
421
422#------------------------------------------------------------------------------
423
424def open(request):
425 """Open the connection."""
426 return True
427
428#------------------------------------------------------------------------------
429
430def prepare_data(pattern, forRead = True):
431 """Prepare the given pattern for reading and/or writing."""
432 return pattern
433
434#------------------------------------------------------------------------------
435
436def read(data):
437 """Read the given data."""
438 return [values.read(offset) for (offset, type) in data]
439
440#------------------------------------------------------------------------------
441
442def close():
443 """Close the connection."""
444 pass
445
446#------------------------------------------------------------------------------
447
448PORT=15015
449
450CALL_READ=1
451CALL_WRITE=2
452CALL_CLOSE=3
453
454RESULT_RETURNED=1
455RESULT_EXCEPTION=2
456
457#------------------------------------------------------------------------------
458
459class Server(threading.Thread):
460 """The server thread."""
461 def __init__(self):
462 """Construct the thread."""
463 super(Server, self).__init__()
464 self.daemon = True
465
466 def run(self):
467 """Perform the server's operation."""
468 serverSocket = socket.socket()
469
470 serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
471 serverSocket.bind(("", PORT))
472
473 serverSocket.listen(5)
474
475 while True:
476 (clientSocket, clientAddress) = serverSocket.accept()
477 thread = threading.Thread(target = self._process, args=(clientSocket,))
478 thread.start()
479
480 def _process(self, clientSocket):
481 """Process the commands arriving on the given socket."""
482 socketFile = clientSocket.makefile()
483 try:
484 while True:
485 (length,) = struct.unpack("I", clientSocket.recv(4))
486 data = clientSocket.recv(length)
487 (call, args) = cPickle.loads(data)
488 exception = None
489
490 try:
491 if call==CALL_READ:
492 result = read(args[0])
493 elif call==CALL_WRITE:
494 result = write(args[0])
495 else:
496 break
497 except Exception, e:
498 exception = e
499
500 if exception is None:
501 data = cPickle.dumps((RESULT_RETURNED, result))
502 else:
503 data = cPickle.dumps((RESULT_EXCEPTION, exception))
504 clientSocket.send(struct.pack("I", len(data)) + data)
505 except Exception, e:
506 print >> sys.stderr, "pyuipc_sim.Server._process: failed with exception:", str(e)
507 finally:
508 try:
509 socketFile.close()
510 except:
511 pass
512 clientSocket.close()
513
514#------------------------------------------------------------------------------
515
516class Client(object):
517 """Client to the server."""
518 def __init__(self, serverHost):
519 """Construct the client and connect to the given server
520 host."""
521 self._socket = socket.socket()
522 self._socket.connect((serverHost, PORT))
523
524 self._socketFile = self._socket.makefile()
525
526 def read(self, data):
527 """Read the given data."""
528 data = cPickle.dumps((CALL_READ, [data]))
529 self._socket.send(struct.pack("I", len(data)) + data)
530 (length,) = struct.unpack("I", self._socket.recv(4))
531 data = self._socket.recv(length)
532 (resultCode, result) = cPickle.loads(data)
533 if resultCode==RESULT_RETURNED:
534 return result
535 else:
536 raise result
537
538#------------------------------------------------------------------------------
539#------------------------------------------------------------------------------
540
541# FIXME: implement proper completion and history
542class CLI(threading.Thread, cmd.Cmd):
543 """The command-line interpreter."""
544 def __init__(self, clientSocket):
545 """Construct the CLI."""
546 self._socket = clientSocket
547 self._socketFile = clientSocket.makefile("rwb")
548
549 threading.Thread.__init__(self)
550 cmd.Cmd.__init__(self,
551 stdin = self._socketFile, stdout = self._socketFile)
552
553 self.use_rawinput = False
554 self.intro = "\nPyUIPC simulator command prompt\n"
555 self.prompt = "PyUIPC> "
556
557 self.daemon = True
558
559 def run(self):
560 """Execute the thread."""
561 try:
562 self.cmdloop()
563 except Exception, e:
564 print "pyuipc_sim.CLI.run: command loop terminated abnormally: " + str(e)
565
566 try:
567 self._socketFile.close()
568 except:
569 pass
570
571 self._socket.close()
572
573 def do_set(self, args):
574 """Handle the set command."""
575 words = args.split()
576 if len(words)==2:
577 variable = words[0]
578 if variable in ["zfw"]:
579 values.__setattr__(variable, float(words[1]))
580 else:
581 print >> self._socketFile, "Unhandled variable: " + variable
582 return False
583
584 def help_set(self):
585 """Print help for the set command."""
586 print >> self._socketFile, "set <variable> <value>"
587
588 def do_quit(self, args):
589 """Handle the quit command."""
590 return True
591
592#------------------------------------------------------------------------------
593
594if __name__ == "__main__":
595 client = Client("127.0.0.1")
596 print client.read([(0x3bfc, "d")])
597else:
598 server = Server()
599 server.start()
600
601#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.