slotUI/SlotCli.py

changeset 115
bb150048d329
parent 114
313aad7435a8
child 116
c2fc650cc48f
equal deleted inserted replaced
114:313aad7435a8 115:bb150048d329
1 #!/usr/bin/env python
2 """
3 Freeslot project
4 Command line interface
5 """
6
7 from freeslot import Blackbox, LOGLEVEL
8 from optparse import OptionParser
9 from operator import itemgetter
10 from subprocess import Popen, PIPE
11 import sys, os
12 from copy import copy
13 import curses
14 from time import sleep
15
16 import SimpleXMLRPCServer
17 import xmlrpclib
18 import threading
19
20
21 VERSION = "1.7"
22 MAXSLOTS = 6
23 TERM = {
24 "caption": "\033[1;37m\033[1;44m",
25 "text": "\033[1;30m",
26 }
27
28 # disable debug log output
29 LOGLEVEL = 10
30
31 SOUNDPREFIX = "quake-"
32 EVENTPREFIX = "event/"
33 SOUNDS = {
34 "countdown_start": os.path.abspath(SOUNDPREFIX + "sound/countdown.mp3"),
35 "race_start": os.path.abspath(SOUNDPREFIX + "sound/racestart.mp3"),
36 "race_prepare": os.path.abspath(SOUNDPREFIX + "sound/prepare.mp3"),
37 "lap_record": os.path.abspath(SOUNDPREFIX + "sound/laprecord.mp3"),
38 "fuel_warning1": os.path.abspath(SOUNDPREFIX + "sound/fuel1.mp3"),
39 "fuel_warning2": os.path.abspath(SOUNDPREFIX + "sound/fuel2.mp3"),
40 "fuel_full": os.path.abspath(SOUNDPREFIX + "sound/fuel_full.mp3"),
41 "pitlane_enter": os.path.abspath(SOUNDPREFIX + "sound/pitlane_enter.mp3"),
42 "pitlane_exit": os.path.abspath(SOUNDPREFIX + "sound/pitlane_exit.mp3"),
43 "data_error": os.path.abspath(SOUNDPREFIX + "sound/data_error.mp3"),
44 "panic": os.path.abspath(SOUNDPREFIX + "sound/panic.mp3"),
45 "panic_shortcut": os.path.abspath(SOUNDPREFIX + "sound/panic_shortcut.mp3"),
46 "resume": os.path.abspath(SOUNDPREFIX + "sound/resume.mp3"),
47 "win": os.path.abspath(SOUNDPREFIX + "sound/win.mp3"),
48
49 }
50
51 def trigger_sound(what):
52 if what in SOUNDS:
53 Popen(["/usr/bin/mpg123", "-q", SOUNDS[what]])
54 #os.spawnlp(os.P_NOWAIT, "/usr/bin/mpg123", "mpg123", SOUNDS[what])
55 #Popen(["/usr/bin/mpg123", SOUNDS[what]]).pid
56
57 def trigger_event(what, slot = 0):
58 trigger_sound(what)
59 Popen(["/bin/sh", os.path.abspath(EVENTPREFIX + what), str(slot)])
60
61 class SlotServer(threading.Thread):
62 def __init__(self, blackbox):
63 threading.Thread.__init__(self)
64 self.server = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", 8000))
65 self.server.register_instance(blackbox)
66 #self.server.register_function(lambda astr: '_' + astr, '_string')
67 self.daemon = True
68
69 def run(self):
70 self.server.serve_forever()
71
72 class SlotClient():
73 def __init__(self, url):
74 self.box = xmlrpclib.Server(url)
75
76 class SlotCli():
77 def __init__(self, test = None, dev=""):
78 self.box = Blackbox()
79 if (not test):
80 self.box.connect(dev)
81 self.rpcserver = SlotServer(self.box)
82 self.rpcserver.start()
83 self.slot_dummy = {
84 "name": "Unnamed",
85 "laps": 0,
86 "laps_last": 0,
87 "last": 0.00,
88 "best": 0.00,
89 "fuel": 0,
90 "fuel_last": 0,
91 "position": 0,
92 "drive": 0,
93 "status": "Idle",
94 "clk": 0,
95 "car": 0,
96 "limit": 15,
97 }
98
99 self.slot = [
100 copy(self.slot_dummy), copy(self.slot_dummy),
101 copy(self.slot_dummy), copy(self.slot_dummy),
102 copy(self.slot_dummy), copy(self.slot_dummy),
103 ]
104 self.reset_slots()
105 self.sysclk = 0.00
106 self.sysclk_last = 0.00
107 self.bestlap = 9999999.00
108 self.test = test
109 self.laplimit = 999;
110 self.timelimit = 0;
111
112 def reset_slots(self):
113 idx = 0
114 for slt in self.slot:
115 slt["laps"] = 0
116 slt["laps_last"] = 0
117 slt["last"] = 0.00
118 slt["best"] = 0.00
119 slt["fuel"] = 100
120 slt["fuel_last"] = 0
121 slt["position"] = idx
122 slt["car"] = idx # used for sort order calculation
123 slt["status"] = self.slot_dummy["status"]
124 slt["clk"] = 0
125 slt["limit"] = 15
126 idx += 1
127 self.bestlap = 99999.00
128 self.raceactive = False
129
130 def update_positions(self):
131 order1 = sorted(self.slot, key=itemgetter(
132 "clk"))
133 order2 = sorted(self.slot, key=itemgetter(
134 "laps"), reverse=True)
135 idx = 1
136 for tst in order2:
137 self.slot[tst["car"]]["position"] = idx
138 idx += 1
139
140 def render_slots(self):
141 self.update_positions()
142 self.scr.addstr(3,0,
143 #"Pos | #/Name | Laps | Best | Last | Fuel | Status ",
144 "Pos | #/Name ",
145 curses.color_pair(2))
146 self.scr.addstr(4,4,
147 " Laps | Best | Last | Fuel | Status ",
148 curses.color_pair(2))
149 for idx in range(MAXSLOTS):
150 """
151 self.scr.addstr((3 + (self.slot[idx]["position"] * 2)), 0,
152 "%3i | %i %15s | %4i | %7.2fs | %7.2fs | %3i%% | %10s" % (
153 self.slot[idx]["position"],
154 self.slot[idx]["car"] + 1, self.slot[idx]["name"],
155 self.slot[idx]["laps"],
156 self.slot[idx]["best"],
157 self.slot[idx]["last"],
158 self.slot[idx]["fuel"],
159 self.slot[idx]["status"],
160 ),
161 curses.color_pair(11 + idx) )
162 """
163 if idx > 3:
164 namesuffix = " (%i)" % self.slot[idx]["drive"]
165 else:
166 namesuffix = ""
167
168 self.scr.addstr((3 + (self.slot[idx]["position"] * 2)), 0,
169 "%3i | %i %15s %48s" % (
170 self.slot[idx]["position"],
171 self.slot[idx]["car"] + 1, (self.slot[idx]["name"] + namesuffix),
172 "",
173 ),
174 curses.color_pair(11 + idx) )
175 self.scr.addstr((4 + (self.slot[idx]["position"] * 2)), 4,
176 " %4i | %7.2fs | %7.2fs | %3i%% | %10s | %2i% 15s" % (
177 self.slot[idx]["laps"],
178 self.slot[idx]["best"],
179 self.slot[idx]["last"],
180 self.slot[idx]["fuel"],
181 self.slot[idx]["status"],
182 self.slot[idx]["limit"],
183 ""
184 ),
185 curses.color_pair(11 + idx) )
186
187 def cleartop(self):
188 self.scr.addstr(0,0, "%60s" % "Race Limits: %i Laps / %i Minutes" % (self.laplimit, self.timelimit))
189 self.scr.addstr(1,0, "%80s" % " ")
190
191 def readName(self, slot):
192 self.scr.nodelay(0) # enable delay on readkey
193 curses.echo()
194 self.scr.addstr(0,0, "Enter Name for Controller %i [%s]:" % (
195 slot + 1,
196 self.slot[slot]["name"]),
197 curses.color_pair(1))
198 self.scr.refresh()
199 name = self.scr.getstr(1,0, 15)
200 if name != "":
201 self.slot[slot]["name"] = name
202 self.cleartop()
203 self.scr.refresh()
204 curses.noecho()
205 self.scr.nodelay(1) # disable delay on readkey
206
207 def readLimit(self, slot):
208 limit = self.readInt("SPEEDLIMIT for %s (%i)" % (
209 self.slot[slot]["name"],
210 slot + 1),
211 self.slot[slot]["limit"], 15)
212 if limit:
213 self.slot[slot]["limit"] = limit
214 self.cleartop()
215 self.box.speedlimit(slot, limit)
216
217
218 def readInt(self, msg, default, maximum = 999999):
219 self.scr.nodelay(0) # enable delay on readkey
220 curses.echo()
221 self.scr.addstr(0,0, "%s [%i]:" % (
222 msg,
223 default),
224 curses.color_pair(1))
225 self.scr.refresh()
226 inp = self.scr.getstr(1,0, 4)
227 if inp != "":
228 try:
229 inp = int(inp)
230 if inp > maximum: inp = maximum
231 except Exception:
232 inp = None
233 else:
234 inp = None
235 self.cleartop()
236 self.scr.refresh()
237 curses.noecho()
238 self.scr.nodelay(1) # disable delay on readkey
239 return inp
240
241
242 def monitor_init(self, live = 1):
243 """
244 Send initializing commands for live monitoring
245 """
246 self.box.query("F1\n") # set fuel logic enabled
247 self.box.query("*%i\n" % live) # set live fuel info
248
249 def monitor_learn(self, slot):
250 # clear garbage in UART rx buffer
251 self.box.query("*0\n") # set live fuel info
252 self.box.query("*0\n") # set live fuel info
253 while self.box.readline() != "": pass
254
255 trk = False
256 spd = 0
257 trk_old = False
258 spd_old = 0
259 clock = -1
260
261 self.monitor_init(slot + 2)
262 while 1:
263 #key = self.scr.getch()
264 #if key == ord('c'): break
265
266 # is there something in the rx buffer?
267 rx = self.box.readline()
268 if (rx != ""):
269 try:
270 data = rx.split(":")
271 if rx[:3] == "LN:":
272 if clock >= 0:
273 clock += 1
274 spd = int(data[1], 16)
275 trk = (data[2] != 'X')
276 if (spd != spd_old) or (trk != trk_old):
277 if clock < 0:
278 clock = 0
279 print "%i,%i,%s" % (clock, spd, trk)
280 trk_old = trk
281 spd_old = spd * 1
282 if rx[:2] == "L:":
283 # update lap time info
284 l = int(data[2], 16)
285 s = int(data[3]) - 1
286 t = int(data[4], 16) / 2000.00
287 if (slot == s):
288 print "# lap %i complete: %3.2f seconds" % (l, t)
289 clock = 0
290 print "%i,%i,%s" % (clock, spd, trk)
291 except:
292 print "RX ERROR: " % rx
293
294 def monitor_playback(self, slot, filename):
295 # clear garbage in UART rx buffer
296 self.box.query("*0\n") # set live fuel info
297 self.box.query("*0\n") # set live fuel info
298 sleep(1)
299 cli.box.speedminimum(slot, 0 )
300 while self.box.readline() != "": pass
301
302 clock = -5
303 trkfile = open(filename, "r").readlines()
304 print "Loading %s..." % filename
305
306 while 1:
307 try:
308 for l in trkfile:
309 l = l.strip()
310 if (l != "") and (l[:1] != "#"):
311 print "Line: %s" % repr(l)
312 data = l.split(",")
313 speed = int(data[1])
314 while (clock < int(data[0]) and (int(data[0]) > 0)):
315 clock += 1
316 sleep(0.07)
317 print "CLK %i/%i -> set: %i" % (clock, int(data[0]), speed)
318 cli.box.speedminimum(slot, speed )
319 # now wait for lap sync :)
320 while self.box.readline() != "": pass
321 rx = ""
322 while rx[:2] != "L:":
323 rx = self.box.readline()
324 data = rx.split(":")
325 l = int(data[2], 16)
326 s = int(data[3]) - 1
327 t = int(data[4], 16) / 2000.00
328 print "# lap %i complete: %3.2f seconds" % (l, t)
329 clock = -3
330 except Exception, e:
331 print repr(e)
332 sys.exit(1)
333 except KeyboardInterrupt:
334 print "resetting"
335 cli.box.speedminimum(slot, 0 )
336 cli.box.speedminimum(slot, 0 )
337 sys.exit(0)
338
339
340
341 def monitor(self):
342 """
343 Live Monitor on the console
344 Keyboard loop to control it???
345 """
346 # clear garbage in UART rx buffer
347 while self.box.readline() != "": pass
348
349 self.monitor_init()
350 self.scr = curses.initscr()
351 curses.start_color()
352 curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) # standard text
353 curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # label
354 curses.init_pair(11, curses.COLOR_BLACK, curses.COLOR_YELLOW) # player 1 slot
355 curses.init_pair(12, curses.COLOR_BLACK, curses.COLOR_GREEN) # player 2 slot
356 curses.init_pair(13, curses.COLOR_BLACK, curses.COLOR_RED) # player 3 slot
357 curses.init_pair(14, curses.COLOR_BLACK, curses.COLOR_MAGENTA) # player 4 slot
358 curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_BLACK) # player 5 slot
359 curses.init_pair(16, curses.COLOR_WHITE, curses.COLOR_BLACK) # player 6 slot
360 curses.noecho() # disable key echo
361 curses.cbreak() # do not buffer keypresses
362 self.scr.keypad(1) # enable special keys
363 self.scr.nodelay(1) # disable delay on readkey
364
365 self.cleartop()
366 self.render_slots()
367 self.scr.refresh()
368
369
370 while 1:
371 key = self.scr.getch()
372 if key == ord('c'): break
373 elif key == ord(' '): self.box.query("+") # panic / resume
374 elif key == 10: self.box.query("#") # remote start button press
375 elif key == ord('1'): self.readName(0)
376 elif key == ord('2'): self.readName(1)
377 elif key == ord('3'): self.readName(2)
378 elif key == ord('4'): self.readName(3)
379 elif key == ord('5'): self.readName(4)
380 elif key == ord('6'): self.readName(5)
381 elif key == ord('q'): self.readLimit(0)
382 elif key == ord('w'): self.readLimit(1)
383 elif key == ord('e'): self.readLimit(2)
384 elif key == ord('r'): self.readLimit(3)
385 elif key == ord('t'): self.readLimit(4)
386 elif key == ord('z'): self.readLimit(5)
387 elif key == ord('a'):
388 if self.slot[4]["drive"] > 0: self.slot[4]["drive"] -= 1
389 cli.box.speedminimum(4, self.slot[4]["drive"])
390 elif key == ord('s'):
391 if self.slot[4]["drive"] < 16: self.slot[4]["drive"] += 1
392 cli.box.speedminimum(4, self.slot[4]["drive"])
393 elif key == ord('y'):
394 if self.slot[5]["drive"] > 0: self.slot[5]["drive"] -= 1
395 cli.box.speedminimum(5, self.slot[5]["drive"])
396 elif key == ord('x'):
397 if self.slot[5]["drive"] < 16: self.slot[5]["drive"] += 1
398 cli.box.speedminimum(5, self.slot[5]["drive"])
399 elif key == ord('t'):
400 tmp = self.readInt("Set new Race TIME limit", self.timelimit)
401 if tmp: self.timelimit = tmp
402 elif key == ord('l'):
403 tmp = self.readInt("Set new Race LAP limit", self.laplimit)
404 if tmp: self.laplimit = tmp
405
406
407 # is there something in the rx buffer?
408 rx = self.box.readline()
409 if (rx != "") or self.test:
410 self.scr.addstr(17,0,
411 "Last RX: %19s" % rx, curses.color_pair(2))
412 self.scr.redrawwin()
413 self.scr.refresh()
414 # we have received something
415 try:
416 data = rx.split(":")
417 if rx[:2] == "L:":
418 # update lap time info
419 l = int(data[2], 16)
420 slot = int(data[3]) - 1
421 t = int(data[4], 16) / 2000.00
422 self.sysclk = int(data[5], 16) / 2000.00
423 self.slot[slot]["laps_last"] = self.slot[slot]["laps"]
424 self.slot[slot]["laps"] = l
425 self.slot[slot]["last"] = t
426 self.slot[slot]["clk"] = self.sysclk
427 if (self.slot[slot]["best"] > t) or (self.slot[slot]["best"] == 0):
428 self.slot[slot]["best"] = t
429 if self.bestlap > t:
430 trigger_event("lap_record", slot + 1)
431 self.bestlap = t
432
433 self.slot[slot]["status"] = "IN-RACE"
434 if (self.slot[slot]["laps_last"] != l) and (l == self.laplimit):
435 # we have lap limit reached!
436 trigger_event("win", slot + 1)
437 self.raceactive = False
438 self.slot[slot]["status"] = "WINNER!"
439 self.box.query("+") # stop race
440
441 self.render_slots()
442
443 if rx[:2] == "F:":
444 # update fuel level
445 slot = int(data[1])
446 f = int(data[2], 16)
447 f = f / 100 # fuel in percent
448 self.sysclk = int(data[3], 16) / 2000.00
449 self.slot[slot]["fuel_last"] = self.slot[slot]["fuel"]
450 self.slot[slot]["fuel"] = f
451 if self.slot[slot]["fuel_last"] != f:
452 if (self.slot[slot]["fuel_last"] == 16) and (f == 15):
453 # 15 percent fuel, set speed limit for car to 8
454 # warning sound
455 trigger_event("fuel_warning1", slot + 1)
456 cli.box.speedlimit(slot, 8)
457 if (self.slot[slot]["fuel_last"] == 6) and (f == 5):
458 # 5 percent, set speed limit for car to 6
459 # warning sound
460 trigger_event("fuel_warning2", slot + 1)
461 cli.box.speedlimit(slot, 6)
462 if (self.slot[slot]["fuel_last"] == 1) and (f == 0):
463 # fuel empty
464 # set speedlimit to 4
465 cli.box.speedlimit(slot, 4)
466 if (self.slot[slot]["fuel_last"] < f) and (f >= 11) and (f < 20):
467 cli.box.speedlimit(slot, 15)
468 if (self.slot[slot]["fuel_last"] < f) and (f == 100):
469 trigger_event("fuel_full", slot + 1)
470 self.render_slots()
471
472 if rx[:1] == "~":
473 # jumpstart occured
474 slot = int(rx[1:2])
475 t = int(data[1], 16) / 2000.00
476 self.slot[slot]["jumpstart"] = t
477 self.slot[slot]["status"] = "Jumpstart!"
478
479 if rx[:3] == "RW:":
480 # ResponseWire packet, do nothing at the moment, just decode
481 slot = int(data[1])
482 devtype = int(data[2])
483 sender = int(data[3], 16)
484 status = int(data[4], 16)
485 self.sysclk = int(data[5], 16)
486 if (devtype == 4):
487 # pitlane sent something
488 if (status == 5):
489 self.slot[slot]["status"] = "PITLANE"
490 trigger_event("pitlane_enter", slot + 1)
491 if (status == 7):
492 self.slot[slot]["status"] = "IN-RACE"
493 trigger_event("pitlane_exit", slot + 1)
494
495 self.render_slots()
496
497 if rx == "!RACE PREPARE":
498 # reset current race status
499 # and display PREPARE PHASE
500 self.reset_slots()
501 for slot in range(MAXSLOTS):
502 self.slot[slot]["status"] = "Prepare"
503 trigger_event("race_prepare")
504
505 if rx == "!RACE START":
506 for slot in range(MAXSLOTS):
507 if self.slot[slot]["status"] == "~~~~~~~~~~":
508 self.slot[slot]["status"] = "Idle"
509 trigger_event("race_start")
510 self.raceactive = True
511
512 if rx == "!COUNTDOWN":
513 # countdown initiated
514 for slot in range(MAXSLOTS):
515 self.slot[slot]["status"] = "~~~~~~~~~~"
516 trigger_event("countdown_start")
517
518 if rx == "!PANIC":
519 # panic mode
520 trigger_event("panic")
521
522 if rx == "!SHORTCUT":
523 # panic mode
524 trigger_event("panic_shortcut")
525
526 if rx == "!RESUME":
527 # panic mode
528 trigger_event("resume")
529
530
531 if ((self.timelimit > 0) and (self.raceactive) and
532 (self.sysclk_last != self.sysclk) and
533 ((self.sysclk / 60) >= self.timelimit)):
534 self.sysclk_last = self.sysclk
535 self.raceactive = False
536 # we have time limit reached!
537 self.box.query("+") # stop race
538 trigger_event("win")
539 # get the one with position 1
540 for slot in self.slots:
541 if slot["position"] == 1:
542 slot["status"] = "WINNER!"
543 self.render_slots()
544 self.sysclk_last = self.sysclk
545
546
547
548 self.scr.addstr(17,31,
549 "Race Timer: %7.3f min" % (self.sysclk / 60),
550 curses.color_pair(2))
551
552 self.scr.refresh()
553
554 except Exception:
555 trigger_event("data_error")
556 pass
557
558 # terminate
559 curses.nocbreak()
560 self.scr.keypad(0)
561 curses.echo()
562 curses.endwin()
563 return None
564
565 def cyclemode(self):
566 pass
567
568 if __name__ == "__main__":
569 parser = OptionParser(version="%prog " + VERSION)
570 parser.add_option("--live", dest="live", action="store_true", default=False,
571 help="Run Live monitor on console", metavar="[0-5]")
572 parser.add_option("--learn", dest="learn", action="store_true", default=False,
573 help="Run Learning mode for [slot]", metavar="[0-5]")
574 parser.add_option("--teach", dest="playback",
575 help="Playback teach file", metavar="[filename]")
576
577 parser.add_option("--slot", dest="carid",
578 help="Required for programming a car directly", metavar="[1-6]")
579 parser.add_option("--fuel", dest="fuel",
580 help="Set maximum CAR fuel level", metavar="[0-15]")
581 parser.add_option("--brake", dest="brake",
582 help="Set CAR brake strength", metavar="[0-15]")
583 parser.add_option("--accel", dest="accel",
584 help="Set CAR acceleration ", metavar="[6-15]")
585 parser.add_option("--blink", dest="blink",
586 help="Set car lights blinking state", metavar="[on|off]")
587 parser.add_option("--limit", dest="limit",
588 help="Controlled SPEED LIMIT (15 = no limit)", metavar="[0-15]")
589 parser.add_option("--drive", dest="drive",
590 help="Controlled SPEED MINIMUM (0 = disabled)", metavar="[0-15]")
591 parser.add_option("--test", dest="test", action="store_true", default=False,
592 help="", metavar="")
593 parser.add_option("--dev", dest="dev", default="/dev/ttyUSB0",
594 help="Communication port", metavar="[/dev/ttyUSB0]")
595
596 (options, args) = parser.parse_args()
597 #if not options.dev:
598 # options.dev = "/dev/ttyUSB0"
599
600 if options.live or options.learn or options.playback:
601 cli = SlotCli(options.test, options.dev)
602 else:
603 cli = SlotClient('http://localhost:8000')
604 # should a CLI function be started?
605
606 if options.live:
607 # start the live monitor
608 cli.monitor()
609 sys.exit(0)
610
611 # check commandline if we have to program something
612 if not options.carid:
613 print "Option --slot is required for all car programming commands!\nUse --help to get a list of available commands"
614 sys.exit(1)
615 else:
616 options.carid = int(options.carid) - 1
617 if (options.carid < 0) or (options.carid > 6):
618 print "Error: Invalid slot selected"
619 sys.exit(1)
620
621 if options.learn:
622 # start the learn monitor
623 cli.monitor_learn(options.carid)
624 sys.exit(0)
625
626 if options.playback:
627 # start the playback monitor
628 cli.monitor_playback(options.carid, options.playback)
629 sys.exit(0)
630
631 if options.fuel:
632 print "setFuel: " + cli.box.progcar(int(options.carid), "fuel", int(options.fuel))
633
634 if options.accel:
635 print "setAccel: " + cli.box.progcar(int(options.carid), "accel", int(options.accel))
636
637 if options.brake:
638 print "setBrake: " + cli.box.progcar(int(options.carid), "brake", int(options.brake))
639
640 if options.blink:
641 state = False
642 if options.blink == "on":
643 state = True
644 print "setBlink: " + cli.box.blinkcar(int(options.carid), state)
645
646 if options.limit:
647 print "Change Speed Limit: " + cli.box.speedlimit(int(options.carid), int(options.limit))
648
649 if options.drive:
650 print "Change minimum Speed drive: " + cli.box.speedminimum(int(options.carid), int(options.drive))
651

mercurial