From 66d1382383e48a09b9af65a86f43f2eb813f83a3 Mon Sep 17 00:00:00 2001 From: Balint Seeber Date: Thu, 4 Dec 2014 18:05:23 -0800 Subject: [PATCH] Moved around dir structure More apps & python libs --- {src/python => apps}/img_rx.py | 0 {src/python => apps}/img_tx.py | 0 apps/realtime_graph_server.py | 234 ++++++++++++++ apps/run_remote_test.py | 39 +++ {src/python => apps}/seq_rx.py | 6 + {src/python => apps}/seq_tx.py | 0 lib/python/.gitignore | 1 + {src => lib}/python/fft_tools.py | 0 lib/python/horizons.py | 434 ++++++++++++++++++++++++++ {src => lib}/python/realtime_graph.py | 172 +++++++++- lib/python/run_remote.py | 142 +++++++++ {src => lib}/python/tcp_server.py | 0 12 files changed, 1021 insertions(+), 7 deletions(-) rename {src/python => apps}/img_rx.py (100%) rename {src/python => apps}/img_tx.py (100%) create mode 100755 apps/realtime_graph_server.py create mode 100755 apps/run_remote_test.py rename {src/python => apps}/seq_rx.py (91%) rename {src/python => apps}/seq_tx.py (100%) create mode 100644 lib/python/.gitignore rename {src => lib}/python/fft_tools.py (100%) create mode 100755 lib/python/horizons.py rename {src => lib}/python/realtime_graph.py (73%) create mode 100755 lib/python/run_remote.py rename {src => lib}/python/tcp_server.py (100%) diff --git a/src/python/img_rx.py b/apps/img_rx.py similarity index 100% rename from src/python/img_rx.py rename to apps/img_rx.py diff --git a/src/python/img_tx.py b/apps/img_tx.py similarity index 100% rename from src/python/img_tx.py rename to apps/img_tx.py diff --git a/apps/realtime_graph_server.py b/apps/realtime_graph_server.py new file mode 100755 index 0000000..b16e571 --- /dev/null +++ b/apps/realtime_graph_server.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# realtime_graph_server.py +# +# Copyright 2014 Balint Seeber +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +import traceback, threading +from optparse import OptionParser +from SimpleXMLRPCServer import SimpleXMLRPCServer + +import realtime_graph# as _realtime_graph + +try: + import matplotlib +except: + matplotlib = None + +#class realtime_graph(_realtime_graph): +# def _create_figure(self, *args, **kwds): +# _realtime_graph._create_figure(*args, **kwds) +# if self.parent is None: +# pass + +class GraphServer(): + def __init__(self, use_queue): + self._use_queue = use_queue + self._graphs = {} + self._last_id = 0 + self._created_event = threading.Event() + self._dispatch_event = threading.Event() + self._processed_event = threading.Event() + self._lock = threading.Lock() + self._queue = [] + #self._last_created_graph = None + self._created_graphs = [] + def get_created_graph(self): + #if self._last_created_graph is None: + if len(self._created_graphs) == 0: + return None + #return self._last_created_graph + with self._lock: + for g in self._created_graphs: + if g.is_created(): + return g + return None + def get_last_graph(self): + if self._last_id == 0: + return None + return self._graphs[self._last_id] + def get_commands(self): + with self._lock: + q = self._queue + self._queue = [] + return q + def has_commands(self): + with self._lock: + return len(self._queue) + def _dispatch(self, method, params): + if method == "_create": + parent_id = params[4] # MAGIC + print "Parent ID:", parent_id + if parent_id is not None: + if self._graphs.has_key(parent_id): + params = list(params) + params[4] = self._graphs[parent_id] + else: + print "Parent graph #%d does not exist" % (parent_id) + g = realtime_graph.realtime_graph(*params) + self._last_id += 1 + self._graphs[self._last_id] = g + print "Created graph #%d:" % (self._last_id), params + if g.is_created(): + #print "Created on creation" + #self._last_created_graph = g + with self._lock: + self._created_graphs += [g] + self._created_event.set() + return self._last_id + + #if method == 'run_event_loop' and self._use_queue: + # # FIXME: Sleep? + # return None + + #if method in ['go_modal']: + # while True: + # with self._lock: + # if len(self._queue) == 0: + # break + # print "Waiting for cleared queue" + # self._processed_event.wait() + # print "Dispatching", method + # res = self.dispatch(method, params) + # print "Done:", res + # return res + + if self._use_queue: + with self._lock: + self._queue += [(method, params)] + self._processed_event.clear() + self._dispatch_event.set() + return None + + return self.dispatch(method, params) + def dispatch(self, method, params): + if len(params) == 0: + print "Not ID supplied for method:", method + return None + graph_id = params[0] + if not isinstance(graph_id, int): + print "First argument not int:", graph_id + return None + params = params[1:] + if not self._graphs.has_key(graph_id): + print "Invalid graph ID:", graph_id + return None + g = self._graphs[graph_id] + if not hasattr(g, method): + print "Invalid method:", method + return None + fn = getattr(g, method) + try: + #print graph_id, method + #print graph_id, method, params + was_created = g.is_created() + res = fn(*params) + if was_created and not g.is_created(): + #if self._last_created_graph == g: + # self._last_created_graph = None + with self._lock: + if g in self._created_graphs: + print "Removing graph #%d from created list:" % (graph_id) + self._created_graphs.remove(g) + else: + print "Graph %d not in created graph list" % (graph_id) + elif not was_created and g.is_created(): + #print "Created on", method + #self._last_created_graph = g + with self._lock: + self._created_graphs += [g] + self._created_event.set() + return res + except Exception, e: + print "Exception while running method '%s' with args:" % (method), params + traceback.print_exc() + return None + +class MySimpleXMLRPCServer(SimpleXMLRPCServer): + pass + #def _dispatch(self, method, params): + +def main(): + parser = OptionParser(usage="%prog: [options]") + + parser.add_option("-a", "--address", type="string", default="0.0.0.0", help="server address [default=%default]") + parser.add_option("-p", "--port", type="int", default=8000, help="server port [default=%default]") + parser.add_option("-s", "--single-thread", action="store_true", default=False, help="run in a single thread [default=%default]") + parser.add_option("-t", "--timeout", type="float", default=0.1, help="GUI event timeout [default=%default]") + + (options, args) = parser.parse_args() + + server = MySimpleXMLRPCServer((options.address, options.port), logRequests=False, allow_none=True) #requestHandler=RequestHandler + + instance = GraphServer(use_queue=not options.single_thread) + server.register_instance(instance) + + font = { + #'family' : 'normal', + #'weight' : 'bold', + 'size' : 10 + } + + if matplotlib is not None: + matplotlib.rc('font', **font) + + if options.single_thread: + try: + server.serve_forever() + except KeyboardInterrupt: + pass + else: + server_thread = threading.Thread(target=server.serve_forever) + server_thread.setDaemon(True) + server_thread.start() + + #print "Waiting for first graph..." + #instance._created_event.wait() + #print "First graph created" + #first_graph = instance.get_last_graph() + + #dummy_graph = realtime_graph.realtime_graph(title="Dummy", show=True) + #instance._created_graphs += [dummy_graph] + + #first_graph = None + have_graph = False + while True: + if instance.get_created_graph() is None: + if not instance.has_commands(): + if have_graph: + print "Waiting..." + have_graph = False + instance._dispatch_event.wait() + instance._dispatch_event.clear() + else: + have_graph = True + instance.get_created_graph().run_event_loop(options.timeout) + + cmds = instance.get_commands() + for cmd in cmds: + instance.dispatch(cmd[0], cmd[1]) + + instance._processed_event.set() + + return 0 + +if __name__ == '__main__': + main() diff --git a/apps/run_remote_test.py b/apps/run_remote_test.py new file mode 100755 index 0000000..79d4184 --- /dev/null +++ b/apps/run_remote_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# run_remote_test.py +# +# Copyright 2014 Balint Seeber +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +try: + import sys; + import run_remote; + if run_remote.run_remote('balint@syskill:Desktop') == True: + sys.exit(0) +except Exception, e: pass + +#exec("try:\n\timport sys; import run_remote;\n\tif run_remote.run_remote('balint@syskill:Desktop') == True: sys.exit(0)\nexcept Exception, e: pass") + +def main(): + print "Hello!" + return 0 + +if __name__ == '__main__': + main() diff --git a/src/python/seq_rx.py b/apps/seq_rx.py similarity index 91% rename from src/python/seq_rx.py rename to apps/seq_rx.py index 0fdd83f..d50fc4d 100755 --- a/src/python/seq_rx.py +++ b/apps/seq_rx.py @@ -37,6 +37,7 @@ def main(): parser.add_option("-L", "--limit", type="int", default=(2**16-1), help="limit [default=%default]") parser.add_option("-i", "--interval", type="int", default=1024, help="update interval [default=%default]") parser.add_option("-l", "--listen", action="store_true", default=False, help="listen on a socket instead of connecting to a server [default=%default]") + parser.add_option("-B", "--transport-buffer-size", type="int", default=1, help="request transport buffer size [default=%default]") (options, args) = parser.parse_args() @@ -104,6 +105,11 @@ def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(destination) + if options.transport_buffer_size > 0: + print "SO_RCVBUF was", s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, options.transport_buffer_size) + print "SO_RCVBUF is", s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + if not hush_open_message: print "Connected" diff --git a/src/python/seq_tx.py b/apps/seq_tx.py similarity index 100% rename from src/python/seq_tx.py rename to apps/seq_tx.py diff --git a/lib/python/.gitignore b/lib/python/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/lib/python/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/src/python/fft_tools.py b/lib/python/fft_tools.py similarity index 100% rename from src/python/fft_tools.py rename to lib/python/fft_tools.py diff --git a/lib/python/horizons.py b/lib/python/horizons.py new file mode 100755 index 0000000..5320a58 --- /dev/null +++ b/lib/python/horizons.py @@ -0,0 +1,434 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# horizons.py +# +# Copyright 2014 Balint Seeber +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +# TO DO +# * Step back sleep*window to calculate doppler delta for single mode + +import sys, time, struct, datetime, threading +import curses +import tzlocal +import numpy +from dateutil import parser as date_parser +from optparse import OptionParser + +_START = "$$SOE" +_END = "$$EOE" + +class horizons_thread(threading.Thread): + def __init__(self, input_path, freq, callback, sleep=1.0, auto_start=True, *args, **kwds): # +ve: downlink, -ve: uplink + threading.Thread.__init__(self, *args, **kwds) + self.setDaemon(True) + self.keep_running = True + self.callback = callback + self.sleep = sleep + self.hint = 0 + self.direction = 1 + self.set_freq(freq) + self.stop_event = threading.Event() + print "Reading \"%s\"" % (input_path) + self.h = horizons(input_path) + if auto_start: + self.start() + def start(self): + print "Starting..." + threading.Thread.start(self) + def stop(self): + if not self.keep_running: + return + print "Stopping..." + self.keep_running = False + self.stop_event.wait() + print "Stopped" + def __del__(self): + print "DTOR" + def set_freq(self, freq): + if freq < 0: + self.direction = -1 + freq = -freq + self.freq = freq + def get(self, freq=None, now_utc=None, hint=None, safe=False): + direction = 1 + if freq is None: + freq = self.freq + direction = self.direction + else: + if freq < 0: + direction = -1 + freq = -freq + + if now_utc is None: + now_utc = datetime.datetime.utcnow() + + if hint is None: + hint = self.hint + + res = self.h.get(now_utc, hint) + + if res is None: + if safe: + #print (None, freq) + return (None, freq) + #print res + return res + + hint, deldot_now, delta_now, ra_now, decl_now, frac = res + + c = 299792458.0 + f_doppler = c / ((c + (direction * deldot_now * 1000)) / freq) + + #print (res, f_doppler), f_doppler-freq + return (res, f_doppler) + + def run(self): + #time.sleep(self.sleep) # HACK: for UI update + + while self.keep_running: + res = self.get() + + if res is None: + break + #print res, res[1]-self.freq + res, f_doppler = res + self.hint, deldot_now, delta_now, ra_now, decl_now, frac = res + + try: + self.callback(f_doppler) + except Exception, e: + print "While executing horizons callback:", e + + time.sleep(self.sleep) + + self.stop_event.set() + +def format_freq(f, decimals=None, units=True, extra=""): + unit = '' + _f = abs(f) + if _f >= 1e9: + f /= 1e9 + unit = 'G' + elif _f >= 1e6: + f /= 1e6 + unit = 'M' + elif _f >= 1e3: + f /= 1e3 + unit = 'k' + if decimals is None: + fmt = "%%%sf" % (extra) + else: + fmt = "%%%s.%df" % (extra, decimals) + freq_str = fmt % f + if units: + freq_str += " %sHz" % (unit) + return freq_str + +class horizons(): + def __init__(self, input_path=None, *args, **kwds): + #self.input_path = input_path + if input_path is not None: + self.parse(input_path) + + def parse(self, input_path): + self.data = [] + + with open(input_path) as f: + lines = f.readlines() + in_data = False + for line in lines: + line = line.strip() + if line == _START: + in_data = True + continue + elif line == _END: + break + if not in_data: + continue + #"2014-May-23 00:00 A 07 34 06.09 +21 55 30.0 n.a. n.a. 0.11457166654073 -3.2683587 50.1314 /T 124.5182" + parts = line.split() + line_time_str = parts[0] + " " + parts[1] + line_time = date_parser.parse(line_time_str) + #print line_time + try: + i = int(parts[2]) + parts = parts[:2] + [''] + parts[2:] + except: + pass + + deldot = float(parts[12]) + #print deldot + delta = float(parts[11]) + ra = float(parts[3]) + (float(parts[4]) / 60.0) + (float(parts[5]) / 3600.0) + decl = float(parts[6]) + (float(parts[7]) / 60.0) + (float(parts[8]) / 3600.0) + + self.data += [{'time':line_time, 'deldot':deldot, 'delta':delta, 'ra':ra, 'decl':decl}] + + return len(self.data) + + def get_line_count(self): return len(self.data) + + def get(self, now_utc, hint=0): + last = None + frac = None + for i in range(hint, len(self.data)): + d = self.data[i] + + if last is not None: + if now_utc >= last['time'] and now_utc < d['time']: + diff = d['time'] - last['time'] + now_diff = now_utc - last['time'] + + frac = now_diff.total_seconds() / diff.total_seconds() + + def _interpolate(key, now, prev, factor): + return prev[key] + ((now[key] - prev[key]) * factor) + + deldot_now = _interpolate('deldot', d, last, frac) + delta_now = _interpolate('delta', d, last, frac) + ra_now = _interpolate('ra', d, last, frac) + decl_now = _interpolate('decl', d, last, frac) + + return ((i - 1), deldot_now, delta_now, ra_now, decl_now, frac) + + last = d + + return None + +def main(): + parser = OptionParser(usage="%prog: [options] ") #option_class=eng_option, + + parser.add_option("-u", "--uplink", type="string", default="", help="uplink frequencies (Hz) [default=%default]") + parser.add_option("-d", "--downlink", type="string", default="", help="downlink frequencies (Hz) [default=%default]") + parser.add_option("-i", "--interval", type="float", default="1.0", help="sleep time (s) [default=%default]") + parser.add_option("-w", "--window", type="int", default="10", help="averaging window size [default=%default]") + parser.add_option("-t", "--time", type="string", default="", help="start time [default=%default]") + parser.add_option("-f", "--time-format", type="string", default="%Y-%m-%d %H:%M:%S", help="time format [default=%default]") + parser.add_option("-o", "--time-offset", type="float", default=0, help="manual time offset (s) [default=%default]") + parser.add_option("-z", "--time-zone", type="float", default=None, help="manual time zone offset (hr) [default=%default]") + parser.add_option("-s", "--single", action="store_true", default=False, help="one calculation [default=%default]") + + (options, args) = parser.parse_args() + + if len(args) < 1: + print "Supply input file" + return 0 + + uplink_freqs, downlink_freqs = [], [] + if len(options.uplink) > 0: + uplink_freqs = map(float, options.uplink.split(',')) + if len(options.downlink) > 0: + downlink_freqs = map(float, options.downlink.split(',')) + + offset = None + if options.time_zone is not None: + offset = datetime.timedelta(hours=options.time_zone) + + custom_start_time = None + if len(options.time) > 0: + try: + l = [] + if len(options.time_format) > 0: + l += [options.time_format] + custom_start_time = datetime.datetime.strptime(options.time, *l) + custom_start_time += datetime.timedelta(seconds=options.time_offset) + print "Starting at: %s (local)" % (custom_start_time) + #raw_input() + except Exception, e: + print "Failed to parse start time: %s" % (options.time) + print e + return + + filename = args[0] + print "Opening", filename + + h = horizons(filename) + + #print "Starting..." + + stdscr = curses.initscr() + + #curses.noecho() + #curses.cbreak() + #stdscr.keypad(1) + #curses.nl / curses.nonl + #stdscr.deleteln() + + ex = None + local_tz = tzlocal.get_localzone() + + uplink_freq_delta = [[]] * len(uplink_freqs) + downlink_freq_delta = [[]] * len(downlink_freqs) + + prev_uplink_doppler_freq = [None] * len(uplink_freqs) + prev_downlink_doppler_freq = [None] * len(downlink_freqs) + + local_start_time = None + start_time = None + if offset is None: + offset = local_tz.utcoffset(datetime.datetime.now()) + + hint = 0 + + try: + while True: + stdscr.erase() + + if custom_start_time is None: + #now = datetime.datetime.now() + now_utc = datetime.datetime.utcnow() + #offset = local_tz.utcoffset(now) + #now_utc = now - offset + now = now_utc + offset + else: + local_now = datetime.datetime.now() + if local_start_time is None: + local_start_time = local_now + now = custom_start_time + (local_now - local_start_time) + now_utc = now - offset + + if start_time is None: + start_time = now + + run_time = now - start_time + + #print "Now:", now + #print "UTC:", now_utc + stdscr.addstr("UTC : %s\n" % (now_utc)) + stdscr.addstr("Local: %s (%+.1f)\n" % (now, (offset.total_seconds()/3600))) + stdscr.addstr("Run : %s\n" % (run_time)) + stdscr.addstr("\n") + + res = h.get(now_utc, hint) + + if res is None: + ex = "Current time is outside range of input file" + break + + hint, deldot_now, delta_now, ra_now, decl_now, frac = res + line_cnt = hint + 1 + + stdscr.addstr("Lines: %d/%d (%d left)\n" % (line_cnt, h.get_line_count(), (h.get_line_count() - line_cnt))) + stdscr.addstr("\n") + + stdscr.addstr("Speed (km/s) : %.7f\n" % (deldot_now)) + stdscr.addstr("Speed (m/s) : %.7f\n" % (deldot_now * 1000)) + stdscr.addstr("Speed (km/hr): %.7f\n" % (deldot_now * 3600)) + stdscr.addstr("\n") + + au_km = 149597870.7 + dist_km = delta_now * au_km + stdscr.addstr("Dist (AU): %.14f\n" % (delta_now)) + stdscr.addstr("Dist (km): %.6f\n" % (dist_km)) + stdscr.addstr("\n") + + c = 299792458.0 + lt = dist_km * 1000.0 / c + stdscr.addstr("Light time (one-way): %f s\n" % (lt)) + stdscr.addstr("Light time (two-way): %f s\n" % (lt*2)) + stdscr.addstr("\n") + + stdscr.addstr("R.A.: %.10f\n" % (ra_now)) + stdscr.addstr("Decl: %+.10f\n" % (decl_now)) + stdscr.addstr("(adjusted for light time)\n") + stdscr.addstr("\n") + + decimals = 9 + + if len(downlink_freqs) > 0: + stdscr.addstr("Downlink frequencies:\n\n") + cnt = 0 + for f in downlink_freqs: + f_doppler = c / ((c + (deldot_now * 1000)) / f) + f_doppler_diff = f_doppler - f + f_doppler_diff_prev = prev_downlink_doppler_freq[cnt] + f_ave_doppler_delta = 0.0 + if f_doppler_diff_prev is not None: + f_doppler_diff_delta = f_doppler_diff - f_doppler_diff_prev + downlink_freq_delta[cnt] += [f_doppler_diff_delta] + if len(downlink_freq_delta[cnt]) > options.window: + downlink_freq_delta[cnt] = downlink_freq_delta[cnt][1:] + f_ave_doppler_delta = numpy.average(downlink_freq_delta[cnt]) + stdscr.addstr("%s: %s (%s @ %s/sec, %s/min)\n" % ( + format_freq(f, decimals=decimals), + format_freq(f_doppler, decimals=decimals), + format_freq(f_doppler_diff, extra="+"), + format_freq(f_ave_doppler_delta, extra="+"), + format_freq(f_ave_doppler_delta * 60.0, extra="+") + )) + prev_downlink_doppler_freq[cnt] = f_doppler_diff + cnt += 1 + stdscr.addstr("\n") + + if len(uplink_freqs) > 0: + stdscr.addstr("Uplink frequencies:\n\n") + cnt = 0 + for f in uplink_freqs: + f_doppler = c / ((c - (deldot_now * 1000)) / f) + f_doppler_diff = f_doppler - f + f_doppler_diff_prev = prev_uplink_doppler_freq[cnt] + f_ave_doppler_delta = 0.0 + if f_doppler_diff_prev is not None: + f_doppler_diff_delta = f_doppler_diff - f_doppler_diff_prev + uplink_freq_delta[cnt] += [f_doppler_diff_delta] + if len(uplink_freq_delta[cnt]) > options.window: + uplink_freq_delta[cnt] = uplink_freq_delta[cnt][1:] + f_ave_doppler_delta = numpy.average(uplink_freq_delta[cnt]) + stdscr.addstr("%s: %s (%s @ %s/sec, %s/min)\n" % ( + format_freq(f, decimals=decimals), + format_freq(f_doppler, decimals=decimals), + format_freq(f_doppler_diff, extra="+"), + format_freq(f_ave_doppler_delta, extra="+"), + format_freq(f_ave_doppler_delta * 60.0, extra="+") + )) + prev_uplink_doppler_freq[cnt] = f_doppler_diff + cnt += 1 + stdscr.addstr("\n") + + stdscr.refresh() + + if options.single: + stdscr.addstr("Press any key to exit...") + stdscr.refresh() + stdscr.getch() + break + + time.sleep(options.interval) + except KeyboardInterrupt: + pass + except Exception, e: + ex = e + + stdscr.erase() + stdscr.refresh() + + curses.nocbreak() + stdscr.keypad(0) + curses.echo() + curses.endwin() + + if ex: + print "Unhandled exception:", ex + + return 0 + +if __name__ == '__main__': + main() diff --git a/src/python/realtime_graph.py b/lib/python/realtime_graph.py similarity index 73% rename from src/python/realtime_graph.py rename to lib/python/realtime_graph.py index 13b984d..25c4363 100644 --- a/src/python/realtime_graph.py +++ b/lib/python/realtime_graph.py @@ -26,11 +26,41 @@ # Detect window close (e.g. wx._core.PyDeadObjectError) # Replace horizontal line code with MPL's in-built one +import os, xmlrpclib, base64 + import numpy -import matplotlib -import matplotlib.pyplot as pyplot -class realtime_graph(): +try: + import matplotlib + import matplotlib.pyplot as pyplot +except Exception, e: + print "Failed to import matplotlib:", e + matplotlib = None + pyplot = None + +def ndarray_serialiser(self, value, write): + write("") + write(base64.b64encode(value.dumps())) + write("\n") +xmlrpclib.Marshaller.dispatch[numpy.ndarray] = ndarray_serialiser + +def ndarray_parser(unmarshaller, data): + unmarshaller.append(numpy.loads(base64.b64decode(data))) + unmarshaller._value = 0 +xmlrpclib.Unmarshaller.dispatch['ndarray'] = ndarray_parser + +def float64_serialiser(self, value, write): + write("") + write(base64.b64encode(value.dumps())) + write("\n") +xmlrpclib.Marshaller.dispatch[numpy.float64] = float64_serialiser + +def float64_parser(unmarshaller, data): + unmarshaller.append(numpy.loads(base64.b64decode(data))) + unmarshaller._value = 0 +xmlrpclib.Unmarshaller.dispatch['float64'] = float64_parser + +class _realtime_graph(): def __init__(self, title="Real-time Graph", sub_title="", x_range=None, show=False, parent=None, manual=False, pos=111, redraw=True, figsize=None, padding=None, y_limits=None, gui_timeout=0.1, data=None, x=None): self.parent = parent @@ -120,6 +150,15 @@ def clear(self, redraw=True): if redraw: self._redraw() + def is_created(self): + return (self.figure is not None) + + def _destroy(self): + self.figure = None + + def _handle_close(self, event): + self._destroy() + def _create_figure(self, data=None, x=None, meta={}, redraw=True, manual=False): if self.parent is None: pyplot.ion() # Must be here @@ -129,6 +168,8 @@ def _create_figure(self, data=None, x=None, meta={}, redraw=True, manual=False): kwds['figsize'] = self.figsize self.figure = pyplot.figure(**kwds) # num=X + self.figure.canvas.mpl_connect('close_event', self._handle_close) + if self.padding is not None: self.figure.subplots_adjust(**self.padding) @@ -294,7 +335,8 @@ def update(self, data=None, title=None, sub_title=None, x=None, meta={}, auto_x_ if points is not None: if clear_existing_points: self.clear_points() - self.add_points(points) + if len(points) > 0: + self.add_points(points) if redraw: self._redraw() @@ -407,10 +449,126 @@ def save(self, output_name): def close(self): pyplot.close(self.figure) + self._destroy() + +_default_remote_address = "" + +class remote_realtime_graph(): #_realtime_graph + def __init__(self, title="Real-time Graph", sub_title="", x_range=None, show=False, parent=None, manual=False, pos=111, redraw=True, figsize=None, padding=None, y_limits=None, gui_timeout=0.1, data=None, x=None, address=""): + if len(address) == 0: + address = _default_remote_address + if len(address) == 0: + raise Exception("Cannot create remote_realtime_graph without an address") + self._proxy = xmlrpclib.ServerProxy(address, allow_none=True) + parent_id = None + if parent is not None: + parent_id = parent._id + self._id = self._proxy._create( + title, + sub_title, + x_range, + show, + parent_id, + manual, + pos, + redraw, + figsize, + padding, + y_limits, + gui_timeout, + data, + x + ) + def __nonzero__(self): + return 1 + # clear + # set_y_limits + # clear_points + # redraw + # run_event_loop + # go_modal + # set_title + # set_sub_title + # remove_horz_line + # remove_vert_line + # save + # close + def _proxy_fn(self, name, *args, **kwds): + #print "Proxying %s:" % (name), args, kwds + fn = getattr(self._proxy, name) + if len(kwds) > 0: + for k in kwds.keys(): + print "Appending to '%s' arg list: %s" % (name, k) + args += kwds[k] + fn(self._id, *args) + def __getattr__(self, name): + return lambda *args, **kwds: self._proxy_fn(name, *args, **kwds) + def set_data(self, data, x=None, meta={}, auto_x_range=True, x_range=None, autoscale=True, redraw=False): + self._proxy.set_data(self._id, + data, + x, + meta, + auto_x_range, + x_range, + autoscale, + redraw + ) + def update(self, data=None, title=None, sub_title=None, x=None, meta={}, auto_x_range=True, x_range=None, autoscale=True, points=None, clear_existing_points=True, redraw=True): + self._proxy.update(self._id, + data, + title, + sub_title, + x, + meta, + auto_x_range, + x_range, + autoscale, + points, + clear_existing_points, + redraw + ) + def add_points(self, points, marker='mo', redraw=False): + self._proxy.add_points(self._id, + points, + marker, + redraw + ) + def add_horz_line(self, value, color='red', linestyle='-', id=None, replace=True, redraw=False): + self._proxy.add_horz_line(self._id, + value, + color, + linestyle, + id, + replace, + redraw + ) + def add_vert_line(self, value, color='black', linestyle='-', id=None, replace=True, redraw=False): + self._proxy.add_vert_line(self._id, + value, + color, + linestyle, + id, + replace, + redraw + ) + +RT_GRAPH_KEY = "RT_GRAPH_ADDR" +if matplotlib and pyplot: + realtime_graph = _realtime_graph +else: + print "Only remote_realtime_graph will be available" + realtime_graph = remote_realtime_graph +if os.environ.has_key(RT_GRAPH_KEY) and len(os.environ[RT_GRAPH_KEY]) > 0: + #global _default_remote_address, realtime_graph + _default_remote_address = os.environ[RT_GRAPH_KEY] + print "Default remote real-time graph address:", _default_remote_address + realtime_graph = remote_realtime_graph def main(): - # FIXME: Plot something simple - return 0 + graph = realtime_graph() + graph.update(numpy.linspace(0, 10)) + graph.go_modal() + return 0 if __name__ == '__main__': - main() + main() diff --git a/lib/python/run_remote.py b/lib/python/run_remote.py new file mode 100755 index 0000000..774ad7c --- /dev/null +++ b/lib/python/run_remote.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# run_remote.py +# +# Copyright 2014 Balint Seeber +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +import sys, subprocess, os, signal, time + +#def signal_term_handler(signal, frame): + ##print 'got SIGTERM' +# return + +def run_remote(remote_host, ignore=[], screen=False, x_forwarding=True, shell="bash"): #, ssh_options=[] + if 'REMOTE_RUN' in os.environ: + #print "Launched remotely:", os.environ['REMOTE_RUN'] + return False + + ignore = map(str.lower, ignore) + base_name = os.path.basename(sys.argv[0]) + if base_name.lower() in ignore: + return False + + #signal.signal(signal.SIGTERM, signal_term_handler) + #print "Installed signal handler" + + #print "Running inside:", sys.argv[0] + #print "Remote host:", remote_host + + #print "Copying..." + remote_copy = ["scp", "-q", sys.argv[0], remote_host] + + #print remote_copy + p = subprocess.Popen(remote_copy) + p.wait() + + #print "Running..." + f = os.path.basename(sys.argv[0]) + idx = remote_host.find(':') + if idx > -1: + rh = remote_host[:idx] + f = remote_host[idx+1:] + '/' + f + else: + rh = remote_host + + ssh_options = [] + if x_forwarding: + ssh_options += ["-X"] + + if screen: + #remote_run = ["ssh", "-t"] + ssh_options + [rh, "REMOTE_RUN=\""+sys.argv[0]+"\" screen -m \""+f+"\""] + remote_run = ["ssh", "-t"] + ssh_options + [rh, "REMOTE_RUN=\""+sys.argv[0]+"\" screen -m "+shell+" -c -- \\\""+f+"\\\""] + else: + remote_run = ["ssh"] + ssh_options + [rh, shell, "-i", "-c", "--", "\"REMOTE_RUN=\\\""+sys.argv[0]+"\\\" \\\""+f+"\\\"\""] + + #print remote_run + p = subprocess.Popen(remote_run) + #print "SSH running" + + #print __file__ + subprocess.Popen(["python", __file__, str(os.getpid()), str(p.pid)]) + + p.wait() + + #print "Exiting..." + sys.exit(0) + return True + +def main(): + if len(sys.argv) < 3: + return + + local_pid = int(sys.argv[1]) + ssh_pid = int(sys.argv[2]) + + #print "Local PID:", local_pid + #print "SSH PID:", ssh_pid + + pids = [local_pid, ssh_pid] + keep_running = True + exited_pid = None + + while keep_running: + for pid in pids: + try: + os.kill(pid, 0) + except OSError: # This will not detect a defunct process + exited_pid = pid + #print "PID %d exited" % (pid) + keep_running = False + break + else: + try: + ps_list = subprocess.check_output(["ps", "-a", "-o", "state,pid"]).split('\n') + for ps in ps_list: + if len(ps) == 0: continue + if ps[0] == 'Z': + parts = ps.split() + if int(parts[1]) == pid: + exited_pid = pid + #print "Zombie process detected:", pid + keep_running = False + break + except Exception, e: + print e + + time.sleep(1.0) # MAGIC + + if exited_pid == local_pid: + try: + os.kill(ssh_pid, 0) + except OSError: + pass + else: + #print "Closing SSH..." + try: + os.kill(ssh_pid, 15) # SIGTERM + #print "SSH closed" + except: + pass + + return 0 + +if __name__ == '__main__': + main() diff --git a/src/python/tcp_server.py b/lib/python/tcp_server.py similarity index 100% rename from src/python/tcp_server.py rename to lib/python/tcp_server.py