#!/usr/bin/env python3 import datetime import io import json import logging import sys try: import urwid except ImportError: logging.error('This program needs urwid') sys.exit(1) try: import serial except ImportError: logging.error('This program needs pySerial') sys.exit(1) class FuzzyAlarmClock: def __init__(self, device): try: self.serial = serial.Serial(device, 9600, timeout=5) except serial.serialutil.SerialException: logging.warning('No such serial device {device}; mocking'.format( device=device )) self.serial = io.StringIO() self.mock = True else: self.mock = False def set_time_to_now(self): now = b's'+datetime.datetime.now().strftime("%y%m%d%u%H%M%S").encode('ascii') self.serial.write(now) def read_alarms(self, n): data = [] self.serial.flushInput() self.serial.write(b'p') for i in range(n): data.append(self.serial.readline().decode('ascii')) for i in range(n): data[i] = data[i].split(' ')[2:-1] days = [] for j in range(8): days.append(bool(int(data[i][0]) & 2**j)) days = days[7:8] + days[:7] data[i] = days+data[i][1:] return data def set_alarm(self, alarm, values): s = 'a'+str(alarm) b0 = 0 for i, bit in enumerate(values[1:8] + values[0:1]): b0 += bit << i s += "%02x" % (b0) for val in values[8:]: s += "%02x" % (val) self.serial.write(s.encode('ascii')) class ModEdit(urwid.IntEdit): def __init__(self, mod, caption="", default=None, offset=0): self.mod = mod self.offset = offset super(ModEdit, self).__init__(caption, default) def insert_text(self, text): super(ModEdit, self).insert_text(text) self.set_edit_text(str( (self.value() - self.offset) % self.mod + self.offset )) class Alarm: def __init__(self, aid, fac): self.w_aid = urwid.Text(str(aid)) self.w_repeat = urwid.CheckBox("") self.w_mon = urwid.CheckBox("") self.w_tue = urwid.CheckBox("") self.w_wed = urwid.CheckBox("") self.w_thu = urwid.CheckBox("") self.w_fri = urwid.CheckBox("") self.w_sat = urwid.CheckBox("") self.w_sun = urwid.CheckBox("") self.w_month = ModEdit(12, offset=1) self.w_day = ModEdit(31, offset=1) self.w_hour = ModEdit(24, default=0) self.w_minute = ModEdit(60, default=0) self.widgets = urwid.Columns( [ self.w_aid, self.w_repeat, self.w_mon, self.w_tue, self.w_wed, self.w_thu, self.w_fri, self.w_sat, self.w_sun, self.w_month, self.w_day, self.w_hour, self.w_minute, ], dividechars=1) def set_values(self, values): self.w_repeat.set_state(values[0]) self.w_mon.set_state(values[1]) self.w_tue.set_state(values[2]) self.w_wed.set_state(values[3]) self.w_thu.set_state(values[4]) self.w_fri.set_state(values[5]) self.w_sat.set_state(values[6]) self.w_sun.set_state(values[7]) self.w_month.set_edit_text(str(values[8])) self.w_day.set_edit_text(str(values[9])) self.w_hour.set_edit_text(str(values[10])) self.w_minute.set_edit_text(str(values[11])) def get_values(self): return [ self.w_repeat.get_state(), self.w_mon.get_state(), self.w_tue.get_state(), self.w_wed.get_state(), self.w_thu.get_state(), self.w_fri.get_state(), self.w_sat.get_state(), self.w_sun.get_state(), self.w_month.value(), self.w_day.value(), self.w_hour.value(), self.w_minute.value(), ] class UI: def __init__(self, fac): self.fac = fac self.alarm_number = 4 self.frame = self.build_ui() self.loop = urwid.MainLoop( self.frame, self.palette, unhandled_input=self.read_commands ) def build_ui(self): self.palette = [ ('header', 'light gray', 'dark blue'), ('footer', 'dark cyan', 'black') ] self.alarms = [] for i in range(self.alarm_number): self.alarms.append(Alarm(i, self.fac)) alarm_headers = urwid.Columns([ urwid.Text('#'), urwid.Text('rep'), urwid.Text('Mon'), urwid.Text('Tue'), urwid.Text('Wed'), urwid.Text('Thu'), urwid.Text('Fri'), urwid.Text('Sat'), urwid.Text('Sun'), urwid.Text('mm'), urwid.Text('dd'), urwid.Text('HH'), urwid.Text('MM'), ], dividechars=1) self.main_body = urwid.ListBox( [alarm_headers] + [al.widgets for al in self.alarms] ) self.footer = urwid.Text('') footer_map = urwid.AttrMap(self.footer, 'footer') header = urwid.Text( 'q: Quit ' + 'r: Read s: Set n: time=Now ' + 'f: to File l: Load ' + '?:Help' ) header_map = urwid.AttrMap(header, 'header') ui = urwid.Frame( self.main_body, footer=footer_map, header=header_map ) return ui def read_commands(self, cmd): { 'n': self.set_time_to_now, 'r': self.read_alarms, 's': self.set_alarms, '?': self.print_help, 'q': self.quit, 'f': self.save_alarms, 'l': self.load_alarms, }.get(cmd, self.do_nothing)() def do_nothing(self): pass def read_alarms(self): alarms = self.fac.read_alarms(self.alarm_number) for i, al in enumerate(alarms): self.alarms[i].set_values(al) self.footer.set_text('Alarms read from device') def set_alarms(self): for i, alarm in enumerate(self.alarms): self.fac.set_alarm(i, alarm.get_values()) self.footer.set_text('Alarms set on device') def set_time_to_now(self): self.fac.set_time_to_now() self.footer.set_text('Time updated on device') def save_alarms(self, fname='saved_alarms.json'): alarms = [a.get_values() for a in self.alarms] with open(fname, 'w') as fp: json.dump(alarms, fp) self.footer.set_text('Alarms saved to {fname}'.format(fname=fname)) def load_alarms(self, fname='saved_alarms.json'): try: with open(fname) as fp: alarms = json.load(fp) except FileNotFoundError: logging.warning('No such file: {fname}'.format(fname=fname)) return False # TODO: do some validation on the data for i, a in enumerate(self.alarms): pass a.set_values(alarms[i]) self.footer.set_text('Alarms loaded from {fname}'.format(fname=fname)) def print_help(self): pass def quit(self): raise urwid.ExitMainLoop() def run(self): self.loop.run() def main(): fac = FuzzyAlarmClock('/dev/ttyUSB0') ui = UI(fac) ui.run() if __name__ == '__main__': main()