#!/usr/bin/env python # -*- coding: utf-8 -*- # # ui.py # # Copyright 2014 Balint Seeber <balint256@gmail.com> # # 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. # # # FIXME: # * Update prediction (using detected bitrate from Network) # * Colour # * Handle when screen size isn't large enough (curses throws ERR) import curses, datetime, math import state from constants import * from primitives import * class Layout(): def __init__(self, name, ui): self.name = name self.ui = ui self.active = False self.y_offset = 0 def draw(self, y): pass def deactivate(self): self.active = False def activate(self, y): self.y_offset = y self.active = True class MinorFrameLayout(Layout): def __init__(self, *args, **kwds): Layout.__init__(self, *args, **kwds) self.deframer = self.ui.engine.deframer self.last_updated_idx = 0 self.changed = False self.prev_frame_idx = 0 def activate(self, y): self.ui.engine.register(EVENT_NEW_BYTE, self) # FIXME: Draw the frame thus far Layout.activate(self, y) def deactivate(self): self.ui.engine.unregister(EVENT_NEW_BYTE, self) Layout.deactivate(self) def __call__(self, *args, **kwds): if not self.active: self.changed = True raise Exception("MinorFrameLayout callback while not active") return stdscr = self.ui.scr byte = kwds['byte'] frame = kwds['frame'] if kwds['idx'] is None: frame_idx = len(frame) - 1 else: frame_idx = kwds['idx'] width = 16 section_length = 8 y_factor = 2 prev_frame_idx = frame_idx - 1 if prev_frame_idx == -1: prev_frame_idx = MINOR_FRAME_LEN - 1 #if prev_frame_idx < len(frame): if True: # FIXME: Being lazy here y = prev_frame_idx / width x = prev_frame_idx % width #stdscr.move(y + y_offset, x * section_length) #stdscr.addstr("%03d %02x " % (prev_frame_idx, frame[prev_frame_idx])) stdscr.move(y*y_factor + self.y_offset, x * section_length + 3) stdscr.addstr(" ") stdscr.move(y*y_factor + self.y_offset, x * section_length + 3 + 3) stdscr.addstr(" ") y = frame_idx / width x = frame_idx % width stdscr.move(y*y_factor + self.y_offset, x * section_length) stdscr.addstr("%03d[%02x]" % (frame_idx, byte)) def draw(self, y): #if not self.changed: # return #self.deframer pass # Purely event driven at the moment class SubcomSubLayout(): def __init__(self, key, subcom_tracker, y_offset): self.key = key self.subcom_tracker = subcom_tracker self.last_updated_idx = None self.y_offset = y_offset def name(self): return self.key class SubcomLayout(Layout): def __init__(self, *args, **kwds): Layout.__init__(self, *args, **kwds) self.subcom_trackers = self.ui.engine.subcom_trackers self.subcom_sublayouts = {} self.width = 16 self.y_factor = 2 self.max_name_len = 0 y = 0 for subcom_key in self.subcom_trackers.keys(): subcom_tracker = self.subcom_trackers[subcom_key] sublayout = SubcomSubLayout(subcom_key, subcom_tracker, y) self.max_name_len = max(self.max_name_len, len(sublayout.name())) self.subcom_sublayouts[subcom_key] = sublayout height = int(math.ceil(1.*subcom_tracker.length / self.width)) * self.y_factor - (self.y_factor - 1) y += (height + 3) self.x_offset = self.max_name_len + 4 # Additional space self.changed = False def draw(self, y): scr = self.ui.scr for subcom_key in self.subcom_trackers.keys(): subcom_tracker = self.subcom_trackers[subcom_key] subcom_sublayout = self.subcom_sublayouts[subcom_key] scr.move(y + subcom_sublayout.y_offset + 2, 1) scr.addstr("%03d" % (subcom_tracker.discontinuity_cnt)) def activate(self, y): for subcom_key in self.subcom_trackers.keys(): self.subcom_trackers[subcom_key].register(EVENT_NEW_BYTE, self) scr = self.ui.scr for subcom_key in self.subcom_sublayouts.keys(): subcom_sublayout = self.subcom_sublayouts[subcom_key] scr.move(y + subcom_sublayout.y_offset, 1) scr.addstr(subcom_sublayout.name()) # FIXME: Draw the frame thus far Layout.activate(self, y) def deactivate(self): for subcom_key in self.subcom_trackers.keys(): self.subcom_trackers[subcom_key].unregister(EVENT_NEW_BYTE, self) Layout.deactivate(self) def __call__(self, event, source, *args, **kwds): if not self.active: self.changed = True raise Exception("SubcomLayout callback while not active") return stdscr = self.ui.scr byte = kwds['byte'] frame = kwds['frame'] frame_idx = len(frame) - 1 sublayout = self.subcom_sublayouts[source.key] section_length = 8 prev_frame_idx = frame_idx - 1 if prev_frame_idx == -1: prev_frame_idx = sublayout.subcom_tracker.length - 1 #if prev_frame_idx < len(frame): if True: # FIXME: Being lazy here y = prev_frame_idx / self.width x = prev_frame_idx % self.width stdscr.move(y*self.y_factor + self.y_offset + sublayout.y_offset, self.x_offset + x * section_length + 3) stdscr.addstr(" ") stdscr.move(y*self.y_factor + self.y_offset + sublayout.y_offset, self.x_offset + x * section_length + 3 + 3) stdscr.addstr(" ") y = frame_idx / self.width x = frame_idx % self.width stdscr.move(self.y_offset + sublayout.y_offset + y*self.y_factor, self.x_offset + x * section_length) stdscr.addstr("%03d[%02x]" % (frame_idx, byte)) class ElementsLayout(Layout): def __init__(self, elements, padding=10, *args, **kwds): Layout.__init__(self, *args, **kwds) self.elements = elements self.max_id_len = 0 self.y_offset_map = {} self.trigger_map = {} self.padding = padding self.last_draw_time = {} self.draw_count = {} self.draw_time_delta = datetime.timedelta(milliseconds=250) self.max_value_len = 0 self.full_refresh = False for element in self.elements: self.last_draw_time[element.id()] = None self.draw_count[element.id()] = 0 self.max_id_len = max(self.max_id_len, len(element.id())) trigger_indices = self.ui.engine.get_element_state(element).get_element().positions().get_trigger_indices(mode=self.ui.engine.options.mode) for trigger_index in trigger_indices: if trigger_index not in self.trigger_map.keys(): self.trigger_map[trigger_index] = [] self.trigger_map[trigger_index] += [element] def activate(self, y): scr = self.ui.scr cnt = 0 self.y_offset_map = {} for element in self.elements: self.y_offset_map[element.id()] = y+cnt self.ui.engine.track(element.positions().get_trigger_indices(mode=self.ui.engine.options.mode), self) scr.move(self.y_offset_map[element.id()], 1) scr.addstr(element.id()) self.draw_element(element) cnt += 1 Layout.activate(self, y) def deactivate(self): for element in self.elements: self.ui.engine.untrack(element.positions().get_trigger_indices(mode=self.ui.engine.options.mode), self) Layout.deactivate(self) def __call__(self, *args, **kwds): trigger = kwds['trigger'] res, map_res = trigger.check_map(self.trigger_map) if not res: raise Exception("%s not in %s" % (trigger, self.trigger_map.keys())) triggered_elements = map_res for element in triggered_elements: self.draw_element(element) def draw_element(self, element): scr = self.ui.scr element_state = self.ui.engine.get_element_state(element) scr.move(self.y_offset_map[element.id()], 1 + self.max_id_len + self.padding) scr.clrtoeol() if element_state.last_value is None: return self.draw_count[element.id()] += 1 count_str = "[%04d]" % element_state.update_count scr.addstr(count_str) s = " = " value_str = element.formatter().format(element_state.last_value) s += value_str if element.unit() is not None and len(element.unit()) > 0: s += " " + element.unit() if element_state.last_valid is not None: if element_state.last_valid == True: s += " (valid)" # FIXME: Green elif element_state.last_valid == False: s += " (invalid)" # FIXME: Red if len(s) > self.max_value_len: self.max_value_len = len(s) self.full_refresh = True scr.addstr(s) if element_state.previous_value is not None: scr.move(self.y_offset_map[element.id()], 1 + self.max_id_len + self.padding + self.max_value_len + 10) # MAGIC s = " (%03d: %s)" % ((self.ui.engine.get_local_time_now() - element_state.previous_value_time).total_seconds(), element.formatter().format(element_state.previous_value)) scr.addstr(s) time_delta = self.ui.engine.get_local_time_now() - element_state.last_update_time time_str = "%03d" % time_delta.total_seconds() scr.move(self.y_offset_map[element.id()], self.ui.max_x - len(time_str)) scr.addstr(time_str) trigger_str = str(element_state.last_trigger) scr.move(self.y_offset_map[element.id()], self.ui.max_x - len(time_str) - 3 - len(trigger_str)) scr.addstr(trigger_str) self.last_draw_time[element.id()] = self.ui.engine.get_local_time_now() def draw(self, y_offset): for element in self.elements: if not self.full_refresh and self.last_draw_time[element.id()] is not None and (self.ui.engine.get_local_time_now() - self.last_draw_time[element.id()]) < self.draw_time_delta: return self.draw_element(element) self.full_refresh = False class HistoryLayout(Layout): def __init__(self, width, elements, *args, **kwds): Layout.__init__(self, *args, **kwds) self.trigger_map = {} self.history_map = {} self.elements = elements self.history_lengths = {} self.width = width for spec in elements: element, history_length = spec self.history_lengths[element] = history_length self.history_map[element] = [] trigger_indices = self.ui.engine.get_element_state(element).get_element().positions().get_trigger_indices(mode=self.ui.engine.options.mode) self.ui.engine.track(trigger_indices, self) for trigger_index in trigger_indices: if trigger_index not in self.trigger_map.keys(): self.trigger_map[trigger_index] = [] self.trigger_map[trigger_index] += [element] self.changed = False def __call__(self, *args, **kwds): self.changed = True trigger = kwds['trigger'] res, map_res = trigger.check_map(self.trigger_map) if not res: raise Exception("%s not in %s" % (trigger, self.trigger_map.keys())) triggered_elements = map_res for element in triggered_elements: element_state = self.ui.engine.get_element_state(element) if element_state.last_value is None: return value_str = element_state.get_element().formatter().format(element_state.last_value) history = self.history_map[element] history += [value_str] diff = len(history) - self.history_lengths[element] if diff > 0: self.history_map[element] = history[diff:] def draw(self, y): if not self.changed: return scr = self.ui.scr x = 8 n = 0 for spec in self.elements: element, history_length = spec history = self.history_map[element] cnt = 0 scr.move(y + cnt, x) scr.addstr(element) cnt += 2 for val in history: if n == 0: scr.move(y + cnt, 0) scr.clrtoeol() scr.move(y + cnt, x) scr.addstr(val) cnt += 1 x += self.width n += 1 class UserInterface(): def __init__(self, engine, timeout=10): self.engine = engine self.timeout = timeout self.scr = None self.active_layout = None self.max_y, self.max_x = 0, 0 self.prev_max_y, self.prev_max_x = 0, 0 self.log_message = "" self.update_log_message = False self.last_engine_state = state.STATE_NONE self.last_active_layout_name = "" self.element_layout_key_shortcuts = {} self.element_layouts = [] self.layout_y_offset = 5 def start(self, element_layouts): self.minor_frame_layout = MinorFrameLayout("raw", self) self.element_layout_key_shortcuts['`'] = self.minor_frame_layout self.subcom_layout = SubcomLayout("subcom", self) self.element_layout_key_shortcuts['~'] = self.subcom_layout print "Building history layout..." history_length = 40 self.history_layout = HistoryLayout(name="history", ui=self, width=24, elements=[ ('hps_1_temp_supercom', history_length), ('hps_2_temp_supercom', history_length), ('hps_1_tc', history_length), #('hps_1_tcX', history_length), ('hps_2_tc', history_length), #('hps_2_tcX', history_length), ('accelerometer', history_length), ]) # MAGIC self.element_layout_key_shortcuts['h'] = self.history_layout print "Building layouts..." for element_layout in element_layouts: name = element_layout[0] shortcut = name[0] if len(element_layout) >= 3: shortcut = element_layout[2] elements = [] for element_name in element_layout[1]: element = self.engine.get_element(element_name, safe=False) if element is None: print "The element '%s' was not found for layout '%s'" % (element_name, name) element = self.engine.get_element(element_name) elements += [element] layout = ElementsLayout(elements, name=name, ui=self) self.element_layouts += [layout] if shortcut not in self.element_layout_key_shortcuts.keys(): self.element_layout_key_shortcuts[shortcut] = layout else: print "ElementLayout '%s' already has shortcut key '%s'" % (self.element_layout_key_shortcuts[shortcut].name, shortcut) self.scr = curses.initscr() #curses.start_color() # FIXME self.scr.timeout(self.timeout) # -1 for blocking self.scr.keypad(1) # Otherwise app will end when pressing arrow keys curses.noecho() #curses.raw() #curses.cbreak() #curses.nl / curses.nonl #self.scr.deleteln() self.switch_layout(self.minor_frame_layout) self.update() #self.scr.refresh() # Done in 'update' def run(self): if not self.handle_keys(): return False self.update() return True def log(self, msg): self.log_message = msg self.update_log_message = True def refresh_screen_state(self): self.max_y, self.max_x = self.scr.getmaxyx() changed = (self.max_y != self.prev_max_y) or (self.prev_max_x != self.max_x) self.prev_max_y, self.prev_max_x = self.max_y, self.max_x return changed def update(self): if self.refresh_screen_state(): self.clear() self.prev_max_y, self.prev_max_x if self.last_engine_state != self.engine.get_state(): self.scr.move(self.max_y-1, 0) self.scr.clrtoeol() self.scr.addstr(state.STATE_TXT[self.engine.get_state()]) self.last_engine_state = self.engine.get_state() if True: self.scr.move(0, 0) #self.scr.clrtoeol() # Don't since current layout name is on RHS self.scr.addstr("Current time: %s" % (self.engine.get_local_time_now())) if self.engine.net.last_enqueue_time: self.scr.move(1, 0) #self.scr.clrtoeol() # Don't since layout shortcuts are on RHS self.scr.addstr("Data arrived: %s" % (self.engine.net.last_enqueue_time)) if True: self.scr.move(2, 0) self.scr.clrtoeol() self.scr.addstr("Data lag : %+f" % (self.engine.net.get_time_diff().total_seconds())) self.scr.move(2, 32) self.scr.addstr("Data source: %s" % (self.engine.net.get_status_string())) self.scr.move(3, 0) self.scr.clrtoeol() self.scr.addstr("Complete frame count: %d, sync reset count: %d, minor frame discontinuities: %d, minor frame index lock: %s, auto minor frame index: %s" % ( self.engine.deframer.get_complete_frame_count(), self.engine.deframer.get_sync_reset_count(), self.engine.frame_tracker.frame_discontinuity_cnt, self.engine.frame_tracker.ignore_minor_frame_idx, self.engine.frame_tracker.minor_frame_idx, )) if self.update_log_message: self.scr.move(self.max_y-2, 0) self.scr.clrtoeol() self.scr.addstr(self.log_message) self.update_log_message = False if self.active_layout: if self.last_active_layout_name != self.active_layout.name: # Screen should have been cleared when changing layout self.scr.move(0, self.max_x - len(self.active_layout.name)) self.scr.addstr(self.active_layout.name) self.last_active_layout_name = self.active_layout.name self.active_layout.draw(self.layout_y_offset) self.scr.refresh() def draw_underlay(self): shortcuts = "".join(self.element_layout_key_shortcuts.keys()) self.scr.move(1, self.max_x - len(shortcuts)) self.scr.addstr(shortcuts) def clear(self): self.scr.erase() self.last_engine_state = None self.last_active_layout_name = "" def switch_layout(self, layout, erase=True): if self.active_layout: self.active_layout.deactivate() if erase: self.clear() self.refresh_screen_state() self.draw_underlay() self.active_layout = layout self.active_layout.activate(self.layout_y_offset) def handle_keys(self): ch = self.scr.getch() if ch > -1: if ch == 27: # ESC (quit) return False elif ch >= ord('0') and ch <= ord('9'): idx = (ch - ord('0') - 1) % 10 if idx < len(self.element_layouts): self.switch_layout(self.element_layouts[idx]) elif ch >= 0 and ch < 256 and chr(ch) in self.element_layout_key_shortcuts.keys(): self.switch_layout(self.element_layout_key_shortcuts[chr(ch)]) else: self.scr.move(self.max_y-3, 0) self.scr.clrtoeol() self.scr.addstr(str(ch)) return True def stop(self): if not self.scr: return self.scr.erase() self.scr.refresh() curses.nocbreak() self.scr.keypad(0) curses.echo() curses.endwin() def main(): return 0 if __name__ == '__main__': main()