mavplayback.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #!/usr/bin/env python
  2. '''
  3. play back a mavlink log as a FlightGear FG NET stream, and as a
  4. realtime mavlink stream
  5. Useful for visualising flights
  6. '''
  7. from __future__ import print_function
  8. from future import standard_library
  9. standard_library.install_aliases()
  10. from builtins import object
  11. import os
  12. import sys
  13. import time
  14. import tkinter
  15. from pymavlink import fgFDM
  16. from argparse import ArgumentParser
  17. parser = ArgumentParser(description=__doc__)
  18. parser.add_argument("--planner", action='store_true', help="use planner file format")
  19. parser.add_argument("--condition", default=None, help="select packets by condition")
  20. parser.add_argument("--gpsalt", action='store_true', default=False, help="Use GPS altitude")
  21. parser.add_argument("--mav10", action='store_true', default=False, help="Use MAVLink protocol 1.0")
  22. parser.add_argument("--out", help="MAVLink output port (IP:port)",
  23. action='append', default=['127.0.0.1:14550'])
  24. parser.add_argument("--fgout", action='append', default=['127.0.0.1:5503'],
  25. help="flightgear FDM NET output (IP:port)")
  26. parser.add_argument("--baudrate", type=int, default=57600, help='baud rate')
  27. parser.add_argument("log", metavar="LOG")
  28. args = parser.parse_args()
  29. if args.mav10:
  30. os.environ['MAVLINK10'] = '1'
  31. from pymavlink import mavutil
  32. filename = args.log
  33. def LoadImage(filename):
  34. '''return an image from the images/ directory'''
  35. app_dir = os.path.dirname(os.path.realpath(__file__))
  36. path = os.path.join(app_dir, 'images', filename)
  37. return tkinter.PhotoImage(file=path)
  38. class App(object):
  39. def __init__(self, filename):
  40. self.root = tkinter.Tk()
  41. self.filesize = os.path.getsize(filename)
  42. self.filepos = 0.0
  43. self.mlog = mavutil.mavlink_connection(filename, planner_format=args.planner,
  44. robust_parsing=True)
  45. self.mout = []
  46. for m in args.out:
  47. self.mout.append(mavutil.mavlink_connection(m, input=False, baud=args.baudrate))
  48. self.fgout = []
  49. for f in args.fgout:
  50. self.fgout.append(mavutil.mavudp(f, input=False))
  51. self.fdm = fgFDM.fgFDM()
  52. self.msg = self.mlog.recv_match(condition=args.condition)
  53. if self.msg is None:
  54. sys.exit(1)
  55. self.last_timestamp = getattr(self.msg, '_timestamp')
  56. self.paused = False
  57. self.topframe = tkinter.Frame(self.root)
  58. self.topframe.pack(side=tkinter.TOP)
  59. self.frame = tkinter.Frame(self.root)
  60. self.frame.pack(side=tkinter.LEFT)
  61. self.slider = tkinter.Scale(self.topframe, from_=0, to=1.0, resolution=0.01,
  62. orient=tkinter.HORIZONTAL, command=self.slew)
  63. self.slider.pack(side=tkinter.LEFT)
  64. self.clock = tkinter.Label(self.topframe,text="")
  65. self.clock.pack(side=tkinter.RIGHT)
  66. self.playback = tkinter.Spinbox(self.topframe, from_=0, to=20, increment=0.1, width=3)
  67. self.playback.pack(side=tkinter.BOTTOM)
  68. self.playback.delete(0, "end")
  69. self.playback.insert(0, 1)
  70. self.buttons = {}
  71. self.button('quit', 'gtk-quit.gif', self.frame.quit)
  72. self.button('pause', 'media-playback-pause.gif', self.pause)
  73. self.button('rewind', 'media-seek-backward.gif', self.rewind)
  74. self.button('forward', 'media-seek-forward.gif', self.forward)
  75. self.button('status', 'Status', self.status)
  76. self.flightmode = tkinter.Label(self.frame,text="")
  77. self.flightmode.pack(side=tkinter.RIGHT)
  78. self.next_message()
  79. self.root.mainloop()
  80. def button(self, name, filename, command):
  81. '''add a button'''
  82. try:
  83. img = LoadImage(filename)
  84. b = tkinter.Button(self.frame, image=img, command=command)
  85. b.image = img
  86. except Exception:
  87. b = tkinter.Button(self.frame, text=filename, command=command)
  88. b.pack(side=tkinter.LEFT)
  89. self.buttons[name] = b
  90. def pause(self):
  91. '''pause playback'''
  92. self.paused = not self.paused
  93. def rewind(self):
  94. '''rewind 10%'''
  95. pos = int(self.mlog.f.tell() - 0.1*self.filesize)
  96. if pos < 0:
  97. pos = 0
  98. self.mlog.f.seek(pos)
  99. self.find_message()
  100. def forward(self):
  101. '''forward 10%'''
  102. pos = int(self.mlog.f.tell() + 0.1*self.filesize)
  103. if pos > self.filesize:
  104. pos = self.filesize - 2048
  105. self.mlog.f.seek(pos)
  106. self.find_message()
  107. def status(self):
  108. '''show status'''
  109. for m in sorted(self.mlog.messages.keys()):
  110. print(str(self.mlog.messages[m]))
  111. def find_message(self):
  112. '''find the next valid message'''
  113. while True:
  114. self.msg = self.mlog.recv_match(condition=args.condition)
  115. if self.msg is not None and self.msg.get_type() != 'BAD_DATA':
  116. break
  117. if self.mlog.f.tell() > self.filesize - 10:
  118. self.paused = True
  119. break
  120. self.last_timestamp = getattr(self.msg, '_timestamp')
  121. def slew(self, value):
  122. '''move to a given position in the file'''
  123. if float(value) != self.filepos:
  124. pos = float(value) * self.filesize
  125. self.mlog.f.seek(int(pos))
  126. self.find_message()
  127. def next_message(self):
  128. '''called as each msg is ready'''
  129. msg = self.msg
  130. if msg is None:
  131. self.paused = True
  132. if self.paused:
  133. self.root.after(100, self.next_message)
  134. return
  135. try:
  136. speed = float(self.playback.get())
  137. except:
  138. speed = 0.0
  139. timestamp = getattr(msg, '_timestamp')
  140. now = time.strftime("%H:%M:%S", time.localtime(timestamp))
  141. self.clock.configure(text=now)
  142. if speed == 0.0:
  143. self.root.after(200, self.next_message)
  144. else:
  145. self.root.after(int(1000*(timestamp - self.last_timestamp) / speed), self.next_message)
  146. self.last_timestamp = timestamp
  147. while True:
  148. self.msg = self.mlog.recv_match(condition=args.condition)
  149. if self.msg is None and self.mlog.f.tell() > self.filesize - 10:
  150. self.paused = True
  151. return
  152. if self.msg is not None and self.msg.get_type() != "BAD_DATA":
  153. break
  154. pos = float(self.mlog.f.tell()) / self.filesize
  155. self.slider.set(pos)
  156. self.filepos = self.slider.get()
  157. if msg.get_type() != "BAD_DATA":
  158. for m in self.mout:
  159. m.write(msg.get_msgbuf())
  160. if msg.get_type() == "GPS_RAW":
  161. self.fdm.set('latitude', msg.lat, units='degrees')
  162. self.fdm.set('longitude', msg.lon, units='degrees')
  163. if args.gpsalt:
  164. self.fdm.set('altitude', msg.alt, units='meters')
  165. if msg.get_type() == "GPS_RAW_INT":
  166. self.fdm.set('latitude', msg.lat/1.0e7, units='degrees')
  167. self.fdm.set('longitude', msg.lon/1.0e7, units='degrees')
  168. if args.gpsalt:
  169. self.fdm.set('altitude', msg.alt/1.0e3, units='meters')
  170. if msg.get_type() == "VFR_HUD":
  171. if not args.gpsalt:
  172. self.fdm.set('altitude', msg.alt, units='meters')
  173. self.fdm.set('num_engines', 1)
  174. self.fdm.set('vcas', msg.airspeed, units='mps')
  175. if msg.get_type() == "ATTITUDE":
  176. self.fdm.set('phi', msg.roll, units='radians')
  177. self.fdm.set('theta', msg.pitch, units='radians')
  178. self.fdm.set('psi', msg.yaw, units='radians')
  179. self.fdm.set('phidot', msg.rollspeed, units='rps')
  180. self.fdm.set('thetadot', msg.pitchspeed, units='rps')
  181. self.fdm.set('psidot', msg.yawspeed, units='rps')
  182. if msg.get_type() == "RC_CHANNELS_SCALED":
  183. self.fdm.set("right_aileron", msg.chan1_scaled*0.0001)
  184. self.fdm.set("left_aileron", -msg.chan1_scaled*0.0001)
  185. self.fdm.set("rudder", msg.chan4_scaled*0.0001)
  186. self.fdm.set("elevator", msg.chan2_scaled*0.0001)
  187. self.fdm.set('rpm', msg.chan3_scaled*0.01)
  188. if msg.get_type() == 'STATUSTEXT':
  189. print("APM: %s" % msg.text)
  190. if msg.get_type() == 'SYS_STATUS':
  191. self.flightmode.configure(text=self.mlog.flightmode)
  192. if msg.get_type() == "BAD_DATA":
  193. if mavutil.all_printable(msg.data):
  194. sys.stdout.write(msg.data)
  195. sys.stdout.flush()
  196. if self.fdm.get('latitude') != 0:
  197. for f in self.fgout:
  198. f.write(self.fdm.pack())
  199. app=App(filename)