PageRenderTime 26ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/streaming/edl.py

https://github.com/emolch/pyrocko
Python | 642 lines | 631 code | 7 blank | 4 comment | 2 complexity | 6869a86795d742687c69e62c72c1d156 MD5 | raw file
Possible License(s): GPL-3.0
  1. # http://pyrocko.org - GPLv3
  2. #
  3. # The Pyrocko Developers, 21st Century
  4. # ---|P------/S----------~Lg----------
  5. from __future__ import absolute_import, print_function
  6. import struct
  7. import collections
  8. import time
  9. import logging
  10. import calendar
  11. import numpy as num
  12. from pyrocko import trace, util
  13. logger = logging.getLogger('pyrocko.streaming.edl')
  14. def hexdump(chars, sep=' ', width=16):
  15. while chars:
  16. line = chars[:width]
  17. chars = chars[width:]
  18. line = line.ljust(width, '\000')
  19. print("%s%s%s" % (
  20. sep.join("%02x" % ord(c) for c in line),
  21. sep, quotechars(line)))
  22. def quotechars(chars):
  23. return ''.join(['.', c][c.isalnum()] for c in chars)
  24. MINBLOCKSIZE = 192
  25. class NotAquiring(Exception):
  26. pass
  27. class ReadError(Exception):
  28. pass
  29. class ReadTimeout(ReadError):
  30. pass
  31. class ReadUnexpected(ReadError):
  32. pass
  33. class GPSError(Exception):
  34. pass
  35. class NoGPS(GPSError):
  36. pass
  37. class NoGPSTime(GPSError):
  38. pass
  39. class GPSTimeNotUTC(GPSError):
  40. pass
  41. block_def = {}
  42. block_def['MOD\0'] = '''
  43. identifier 4s
  44. size_of_mod I
  45. device_id 12s
  46. version 6s
  47. space1 1s
  48. serial_no 4s
  49. space2 1s
  50. test_pattern 8s
  51. block_count I
  52. ncomps H
  53. sample_rate H
  54. bytes_per_sample H
  55. filter_type 2s
  56. decimation3 H
  57. decimation4 H
  58. plldata h
  59. gain_g 1s
  60. gain B
  61. gain_component1 I
  62. gain_component2 I
  63. gain_component3 I
  64. offset_component1 I
  65. offset_component2 I
  66. offset_component3 I
  67. supply_voltage H
  68. supply_current H
  69. temp_sensor_voltage H
  70. supply_voltage_remote H
  71. user_input1 H
  72. user_input2 H
  73. user_input3 H
  74. not_used H
  75. coupling 2s
  76. reserved1 I
  77. reserved2 I
  78. reserved3 I
  79. gps_status_flags H
  80. gps_message_block_count I
  81. gps_message 72s
  82. '''.split()
  83. block_def['MDE\0'] = '''
  84. identifier 4s
  85. size_of_mde I
  86. serial_no 8s
  87. decimation5 H
  88. decimation6 H
  89. decimation7 H
  90. gain_component4 I
  91. gain_component5 I
  92. gain_component6 I
  93. offset_component4 I
  94. offset_component5 I
  95. offset_component6 I
  96. temperature1 H
  97. temperature2 H
  98. temperature3 H
  99. temperature4 H
  100. temperature5 H
  101. temperature6 H
  102. gps_message 129s
  103. pad 1s
  104. '''.split()
  105. block_def['SUM\0'] = '''
  106. identifier 4s
  107. size_of_sum I
  108. reserved H
  109. checksum_lo B
  110. checksum_hi B
  111. '''.split()
  112. block_def['DAT\0'] = '''
  113. identifier 4s
  114. size_of_dat I
  115. '''.split()
  116. Blocks = {}
  117. for k in block_def.keys():
  118. fmt = '<'+''.join(block_def[k][1::2])
  119. fmt_len = struct.calcsize(fmt)
  120. Block = collections.namedtuple('Block'+k[:3], block_def[k][::2])
  121. Blocks[k] = (fmt, fmt_len, Block)
  122. gps_fmt = {}
  123. # gps altitude message
  124. gps_fmt['AL'] = '''
  125. type 2 str
  126. time_of_day 5 float
  127. altitude 6 float
  128. vertical_velocity 4 float
  129. source_flag 1 int
  130. age_flag 1 int
  131. '''
  132. # gps position velocity message
  133. gps_fmt['PV'] = '''
  134. type 2 str
  135. time_of_day 5 float
  136. latitude 8 lat_float
  137. longitude 9 lon_float
  138. speed_mph 3 float
  139. heading 3 float
  140. source_flag 1 int
  141. age_flag 1 int
  142. '''
  143. # gps status message
  144. gps_fmt['ST'] = '''
  145. type 2 str
  146. tracking_status_code 2 hex_int
  147. nibble1 1 hex_int
  148. nibble2 1 hex_int
  149. machine_id 2 hex_int
  150. nibble3 1 hex_int
  151. nibble4 1 hex_int
  152. reserved 2 str
  153. '''
  154. # gps time date message
  155. gps_fmt['TM'] = '''
  156. type 2 str
  157. hours 2 int
  158. minutes 2 int
  159. seconds 5 seconds_float
  160. day 2 int
  161. month 2 int
  162. year 4 int
  163. gps_utc_time_offset 2 int
  164. current_fix_source 1 int
  165. number_usable_svs 2 int
  166. gps_utc_offset_flag 1 int
  167. reserved 5 str
  168. '''
  169. def latlon_float(s):
  170. return int(s) / 100000.
  171. def seconds_float(s):
  172. return int(s) / 1000.
  173. def hex_int(s):
  174. return int(s, 16)
  175. convert_functions = {
  176. 'int': int,
  177. 'float': float,
  178. 'lat_float': latlon_float,
  179. 'lon_float': latlon_float,
  180. 'seconds_float': seconds_float,
  181. 'str': str,
  182. 'hex_int': hex_int}
  183. class GPSFormat_(object):
  184. def __init__(self, name, fmt):
  185. nn = 0
  186. names = []
  187. items = []
  188. for line in fmt.strip().splitlines():
  189. toks = line.split()
  190. n = int(toks[1])
  191. items.append((nn, nn+n, convert_functions[toks[2]]))
  192. names.append(toks[0])
  193. nn += n
  194. self.items = items
  195. self.Message = collections.namedtuple('GPSMessage'+k, names)
  196. def unpack(self, s):
  197. return self.Message(
  198. *(converter(s[begin:end])
  199. for (begin, end, converter) in self.items))
  200. GPSFormat = {}
  201. for k in gps_fmt.keys():
  202. GPSFormat[k] = GPSFormat_(k, gps_fmt[k])
  203. def portnames():
  204. try:
  205. # needs serial >= v2.6
  206. from serial.tools.list_ports import comports
  207. names = sorted(x[0] for x in comports())
  208. except Exception:
  209. # may only work on some linuxes
  210. from glob import glob
  211. names = sorted(glob('/dev/ttyS*') + glob('/dev/ttyUSB*'))
  212. return names
  213. def unpack_block(data):
  214. block_type = data[:4]
  215. if block_type not in Blocks:
  216. return None
  217. fmt, fmt_len, Block = Blocks[block_type]
  218. if len(data) < fmt_len:
  219. raise ReadError('block size too short')
  220. return Block(*struct.unpack(fmt, data[:fmt_len])), data[fmt_len:]
  221. def unpack_values(ncomps, bytes_per_sample, data):
  222. if bytes_per_sample == 4:
  223. return num.frombuffer(data, dtype=num.dtype('<i4'))
  224. # 3-byte mode is broken:
  225. # elif bytes_per_sample == 3:
  226. # b1 = num.frombuffer(data, dtype=num.dtype('<i1'))
  227. # b4 = num.zeros(len(data)/4, dtype=num.dtype('<i4'))
  228. # b4.view(dtype='<i2')[::2] = b1.view(dtype='<i2')
  229. # b4.view(dtype='<i1')[2::4] = b1[i::3]
  230. # return b4.astype(num.int32)
  231. else:
  232. raise ReadError('unimplemented bytes_per_sample setting')
  233. class TimeEstimator(object):
  234. def __init__(self, nlookback):
  235. self._nlookback = nlookback
  236. self._queue = []
  237. self._t0 = None
  238. self._n = 0
  239. self._deltat = None
  240. def insert(self, deltat, nadd, t):
  241. if self._deltat is None or self._deltat != deltat:
  242. self.reset()
  243. self._deltat = deltat
  244. if self._t0 is None and t is not None:
  245. self._t0 = int(round(t/self._deltat))*self._deltat
  246. if t is None:
  247. self._n += nadd
  248. if self._t0:
  249. return self._t0 + (self._n-nadd)*self._deltat
  250. else:
  251. return None
  252. self._queue.append((self._n, t))
  253. self._n += nadd
  254. while len(self._queue) > self._nlookback:
  255. self._queue.pop(0)
  256. ns, ts = num.array(self._queue, dtype=float).T
  257. tpredicts = self._t0 + ns * self._deltat
  258. terrors = ts - tpredicts
  259. mterror = num.median(terrors)
  260. print(mterror / deltat, '+-', num.std(terrors) / deltat)
  261. if num.abs(mterror) > 0.75*deltat and \
  262. len(self._queue) == self._nlookback:
  263. t0 = self._t0 + mterror
  264. self._queue[:] = []
  265. self._t0 = int(round(t0/self._deltat))*self._deltat
  266. return self._t0 + (self._n-nadd)*self._deltat
  267. def reset(self):
  268. self._queue[:] = []
  269. self._n = 0
  270. self._t0 = None
  271. def __len__(self):
  272. return len(self._queue)
  273. class GPSRecord(object):
  274. def __init__(self, al, pv, st, tm):
  275. self._al = al
  276. self._pv = pv
  277. self._st = st
  278. self._tm = tm
  279. @property
  280. def time(self):
  281. if not self._st.tracking_status_code == 0:
  282. raise NoGPSTime()
  283. if not self._tm.gps_utc_offset_flag:
  284. raise GPSTimeNotUTC()
  285. tm = self._tm
  286. return util.to_time_float(calendar.timegm((
  287. tm.year, tm.month, tm.day, tm.hours, tm.minutes, tm.seconds)))
  288. @property
  289. def latitude(self):
  290. return self._pv.latitude
  291. @property
  292. def longitude(self):
  293. return self._pv.longitude
  294. @property
  295. def altitude(self):
  296. return self._al.altitude
  297. def __str__(self):
  298. try:
  299. stime = util.time_to_str(self.time)
  300. except GPSError:
  301. stime = '?'
  302. return '''%s %s %s %s''' % (
  303. stime, self.latitude, self.longitude, self.altitude)
  304. def stime_none_aware(t):
  305. if t is None:
  306. return '?'
  307. else:
  308. return util.time_to_str(t)
  309. class Record(object):
  310. def __init__(self, mod, mde, dat, sum, values):
  311. self._mod = mod
  312. self._mde = mde
  313. self._dat = dat
  314. self._sum = sum
  315. self._values = values
  316. self._approx_system_time = None
  317. self._approx_gps_time = None
  318. self._gps = None
  319. def set_approx_times(
  320. self, approx_system_time, approx_gps_time, measured_system_time):
  321. self._approx_system_time = approx_system_time
  322. self._approx_gps_time = approx_gps_time
  323. self._measured_system_time = measured_system_time
  324. @property
  325. def time(self):
  326. if self._mod.reserved1 != 0:
  327. return float(self._mod.reserved1)
  328. return self._approx_system_time
  329. @property
  330. def traces(self):
  331. traces = []
  332. for i in range(self._mod.ncomps):
  333. tr = trace.Trace(
  334. '', 'ed', '', 'p%i' % i,
  335. deltat=float(self._mod.ncomps)/self._mod.sample_rate,
  336. tmin=self.time,
  337. ydata=self._values[i::3])
  338. traces.append(tr)
  339. traces.extend(self.traces_delays())
  340. return traces
  341. def traces_delays(self):
  342. traces = []
  343. for name, val in (
  344. ('gp', self.gps_time_or_none),
  345. ('sm', self._measured_system_time),
  346. ('sp', self._approx_system_time)):
  347. if val is not None:
  348. tr = trace.Trace(
  349. '', 'ed', name, 'del',
  350. deltat=1.0,
  351. tmin=self.time,
  352. ydata=num.array([val - self.time]))
  353. traces.append(tr)
  354. return traces
  355. def _gps_messages(self):
  356. for line in self._mde.gps_message.splitlines():
  357. if len(line) > 4 and line[0] == '>' and line.rstrip()[-1] == '<':
  358. yield GPSFormat[line[2:4]].unpack(line[2:])
  359. @property
  360. def gps(self):
  361. if self._mod.block_count != self._mod.gps_message_block_count:
  362. raise NoGPS()
  363. if self._gps is not None:
  364. return self._gps
  365. kwargs = {}
  366. for mess in self._gps_messages():
  367. kwargs[mess.type.lower()] = mess
  368. if sorted(kwargs.keys()) == ['al', 'pv', 'st', 'tm']:
  369. self._gps = GPSRecord(**kwargs)
  370. return self._gps
  371. else:
  372. raise NoGPS()
  373. @property
  374. def gps_time_or_none(self):
  375. try:
  376. return self.gps.time
  377. except GPSError:
  378. return None
  379. def __str__(self):
  380. return '\n'.join([
  381. '%s' % str(x) for x in (self._mod, self._mde)]) + '\n'
  382. def str_times(self):
  383. return '''--- Record ---
  384. Time GPS: %s (estimated) %s (measured)
  385. Time system: %s (estimated) %s (measured)
  386. ''' % tuple([stime_none_aware(t) for t in (
  387. self._approx_gps_time,
  388. self.gps_time_or_none,
  389. self._approx_system_time,
  390. self._measured_system_time)])
  391. class Reader(object):
  392. def __init__(self, port=0, timeout=2., baudrate=115200, lookback=30):
  393. if isinstance(port, int):
  394. self._port = portnames()[port]
  395. else:
  396. self._port = str(port)
  397. self._timeout = float(timeout)
  398. self._baudrate = int(baudrate)
  399. self._serial = None
  400. self._buffer = ''
  401. self._irecord = 0
  402. self._time_estimator_system = TimeEstimator(lookback)
  403. self._time_estimator_gps = TimeEstimator(lookback)
  404. def running(self):
  405. return self._serial is not None
  406. def assert_running(self):
  407. if not self.running():
  408. raise NotAquiring()
  409. def start(self):
  410. self.stop()
  411. import serial
  412. self._serial = serial.Serial(
  413. port=self._port,
  414. baudrate=self._baudrate,
  415. timeout=self._timeout)
  416. self._sync_on_mod()
  417. def _sync_on_mod(self):
  418. self._fill_buffer(MINBLOCKSIZE)
  419. while self._buffer[:4] != 'MOD\0':
  420. imark = self._buffer.find('MOD\0')
  421. if imark != -1:
  422. self._buffer = self._buffer[imark:]
  423. else:
  424. self._buffer = self._buffer[-4:]
  425. self._fill_buffer(MINBLOCKSIZE)
  426. def _fill_buffer(self, minlen):
  427. if len(self._buffer) >= minlen:
  428. return
  429. nread = minlen-len(self._buffer)
  430. try:
  431. data = self._serial.read(nread)
  432. hexdump(data)
  433. except Exception:
  434. raise ReadError()
  435. if len(data) != nread:
  436. self.stop()
  437. raise ReadTimeout()
  438. self._buffer += data
  439. def _read_block(self, need_block_type=None):
  440. self.assert_running()
  441. self._fill_buffer(8)
  442. block_type, block_len = struct.unpack('<4sI', self._buffer[:8])
  443. if need_block_type is not None and block_type != need_block_type:
  444. raise ReadUnexpected()
  445. block_len += 8
  446. self._fill_buffer(block_len)
  447. block_data = self._buffer
  448. self._buffer = ''
  449. return unpack_block(block_data)
  450. def read_record(self):
  451. self._irecord += 1
  452. mod, _ = self._read_block('MOD\0')
  453. measured_system_time = time.time() - 4.0
  454. mde, _ = self._read_block('MDE\0')
  455. dat, values_data = self._read_block('DAT\0')
  456. sum, _ = self._read_block('SUM\0')
  457. values = unpack_values(mod.ncomps, mod.bytes_per_sample, values_data)
  458. deltat = 1./mod.sample_rate * mod.ncomps
  459. r = Record(mod, mde, dat, sum, values)
  460. approx_system_time = self._time_estimator_system.insert(
  461. deltat, values.size//mod.ncomps, measured_system_time)
  462. try:
  463. gpstime = r.gps.time
  464. except GPSError:
  465. gpstime = None
  466. approx_gps_time = self._time_estimator_gps.insert(
  467. deltat, values.size//mod.ncomps, gpstime)
  468. r.set_approx_times(
  469. approx_system_time, approx_gps_time, measured_system_time)
  470. return r
  471. def stop(self):
  472. if not self.running():
  473. return
  474. self._serial.close()
  475. self._serial = None
  476. self._buffer = ''
  477. self._time_estimator_system.reset()
  478. self._time_estimator_gps.reset()
  479. class EDLHamster(object):
  480. def __init__(self, *args, **kwargs):
  481. self.reader = Reader(*args, **kwargs)
  482. def acquisition_start(self):
  483. self.reader.start()
  484. def acquisition_stop(self):
  485. self.reader.stop()
  486. def process(self):
  487. rec = self.reader.read_record()
  488. self.got_record(rec)
  489. def got_record(self, rec):
  490. for tr in rec.traces:
  491. self.got_trace(tr)
  492. def got_trace(self, tr):
  493. logger.info('Got trace from EDL: %s' % tr)