def outputD(c): '''output def(c = commander handle)''' uiq.put(('UI thread initialised', 'DEBUG')) while True: updateUItime(c) if uiq.empty(): time.sleep(cfg.ui_timeupdate) else: item = uiq.get() if type(item) == str: item = (item, 'NORMAL') # No message priority was specified # if item[1] == 'DEBUG': # try: item[2] # except IndexError: # item = (*item, 1) # Default to debug level 1 # if type(item[2]) != int: # uiq("Invalid debug level encountered", 'ERR') # elif cfg.ui_debug < item[2]: # Debug level is too low to show this item # continue if cfg.ui_ts: if cfg.ui_tscut > 0: ts = datetime.now().strftime( cfg.ui_tsfmt )[:-cfg.ui_tscut] + " - " # Format timestamp else: ts = datetime.now().strftime(cfg.ui_tsfmt) + " - " item = (ts + item[0].replace('\n', '\n' + ' ' * len(ts)), item[1] ) # Add timestamp and spaces to align multiline output c.output(item[0], cfg.ui_colors[item[1]])
def debug(msg, level=1): '''debugging output, level = debug level (default 1)''' try: if cfg.ui_debuglevel[threading.current_thread().name] >= level: uiq.put((msg, 'DEBUG')) except KeyError: # This is necessary for when environment.py is called directly pass
def pendulumTimeout(): '''pendulumTimeout() is called when the pendulum doesn't arrive for cfg.p_timeout seconds ''' global timeoutrpt, watchdog, pa, pd, prevArr, prevDep watchdog.reset() # Need this to trigger again in cfg.p_timeout seconds timeoutrpt += 1 uiq.put(('WARNING: Beat timeout after {} seconds! ({}/{})'.format( cfg.p_timeout, timeoutrpt, cfg.p_timeoutrpt), 'WARN')) if timeoutrpt == cfg.p_timeoutrpt: # We've reached the maximum number of misses timeoutrpt = 0 # Reset this for when the clock restarts uiq.put(('ERROR: Too many missed beats. Resetting monitor thread.', 'ERR')) globs.beatbanner = "[ Waiting for first beat ]" prevArr = 0 prevDep = 0 watchdog.stop() # Kill the watchdog try: cfg.p_timeoutcmd except AttributeError: pass else: uiq.put( ('Executing timeout command \'{}\''.format(cfg.p_timeoutcmd), 'DEBUG')) try: tocmdout = subprocess.check_output([cfg.p_timeoutcmd]) if len(tocmdout) > 0: uiq.put((tocmdout.decode('ascii'), 'DEBUG')) except subprocess.CalledProcessError as err: uiq.put(('ERROR: {}'.format(err), 'ERR'))
def switchdebug(mname, mcode, mode=None): '''switch debug modes interactively''' if not mode: uiq.put( ('{} debug output level is {}'.format(mname, cfg.ui_debuglevel[mcode]))) # if cfg.ui_debuglevel[mcode]: uiq.put(('{} debug output is on'.format(mname))) # else: uiq.put(('{} debug output is off'.format(mname))) return else: try: newlevel = int(mode) if not 0 <= newlevel <= 3: raise ValueError cfg.ui_debuglevel[mcode] = newlevel except ValueError: uiq.put( ('ERROR: {} debug setting must be 0-3'.format(mname), 'ERR')) # elif mode.lower() == 'on': # cfg.ui_debugmod[mcode] = True # switchdebug(mname, mcode) # uiq.put(('{} debug output is on'.format(mname))) # elif mode.lower() == 'off': # cfg.ui_debugmod[mcode] = False # globals()[varname] = False # uiq.put(('{} debug output is off'.format(mname))) # else: # uiq.put(('ERROR: {} debug setting must be \'on\' or \'off\''.format(mname), 'ERR')) switchdebug(mname, mcode)
def __call__(self,line): tokens=line.split() cmd=tokens[0].lower() args=tokens[1:] if cmd in self._quit_cmd: return Commander.Exit elif cmd in self._help_cmd: return self.help(args[0] if args else None) elif hasattr(self, 'do_'+cmd): return getattr(self, 'do_'+cmd)(*args) else: uiq.put(('Unknown command \'{}\''.format(line),'WARN'))
def envD(pig): '''environmental data thread (pig = pigpiod handle)''' debug('Environmental data thread initialising') if cfg.env_frequency - 3 * cfg.env_delay < 0: uiq.put(('ERROR: env_frequency too low (must be at least {})'.format( 3 * cfg.env_delay), 'ERR')) return while True: globs.temperature, globs.humidity = temphumid(pig) debug( 'Read temperature {} and humidity {}'.format( globs.temperature, globs.humidity), 2) time.sleep(cfg.env_frequency - 3 * cfg.env_delay)
def pendulumD(pig): '''pendulum monitoring thread ''' pig.set_mode(cfg.p_gpio_irsense_pin, pigpio.INPUT) pig.set_glitch_filter(cfg.p_gpio_irsense_pin, cfg.p_glitchfilter) global prevArr, prevDep, watchdog, pa, pd prevArr = 0 prevDep = 0 # Set a callback for every pendulum cross pig.set_pull_up_down(cfg.p_gpio_irsense_pin, pigpio.PUD_OFF) pd = pig.callback(cfg.p_gpio_irsense_pin, pigpio.RISING_EDGE, pendulumDepart) pa = pig.callback(cfg.p_gpio_irsense_pin, pigpio.FALLING_EDGE, pendulumArrive) uiq.put(('pendulum monitor thread initialised', 'DEBUG'))
def do_env(self, *args): '''set/display environmental sensor parameters Usage: env [cmd] [args] where cmd/args are: (nothing) display environmental sensor status debug display/set environmental sensor debug output (none) display environmental sensor debug output status [0-3] set environmental sensor debug output level ''' if len(args) == 0: if cfg.env_engine: uiq.put( ('Current temperature is {} C and humidity is {}%'.format( globs.temperature, globs.humidity))) else: uiq.put(('Environmental sensors disabled')) else: cmd = args[0].lower() if cmd == 'debug': if len(args) == 1: switchdebug('Environmental sensors', 'env') else: switchdebug('Environmental sensors', 'env', args[1])
def dbopen(): '''open the database ''' global dbx dbx = None debug('Opening database', 2) # if cfg.ui_debugmod['db']: uiq.put(('Opening database', 'DEBUG', 2)) try: dbx = sqlite3.connect(cfg.db_file) # dbx.isolation_level = 'EXCLUSIVE' # FIXME - this seems not to lock against other processes # dbx.execute('BEGIN EXCLUSIVE') sql = "CREATE TABLE IF NOT EXISTS beats ({});".format(globs.sqltable) cur = dbx.cursor() try: cur.execute(sql) dbx.commit() except Error as dberr: uiq.put(('Database table create error: {}'.format(dberr), 'ERR')) except Error as dberr: uiq.put(('Database open error: {}'.format(dberr), 'ERR')) return dbx
def do_db(self, *args): '''set/display database parameters Usage: db [cmd] [args] where cmd/args are: (nothing) display database engine status debug display/set database debug output (none) display database debug output status [0-3] set database debug output level ''' if len(args) == 0: if cfg.db_engine: uiq.put(('Database storage is on')) else: uiq.put(('Database storage is off')) else: cmd = args[0].lower() if cmd == 'debug': if len(args) == 1: switchdebug('Database', 'db') else: switchdebug('Database', 'db', args[1]) # if cfg.ui_debug_db: uiq.put(('Database debug output is off')) # else: uiq.put(('Database debug output is on')) # elif args[1].lower() == 'on': # cfg.ui_debug_db = True # uiq.put(('Database debug output is on')) # elif args[1].lower() == 'off': # cfg.ui_debug_db = False # uiq.put(('Database debug output is off')) # else: # uiq.put(('ERROR: Database debug setting must be \'on\' or \'off\'', 'ERR')) # return else: uiq.put(('ERROR: Invalid db command', 'ERR'))
def pendulumDepart(g, L, t): '''pendulumDepart(gpio, level, tick) - pendulum has left IR sensor gate ''' global prevArr, prevDep delta = t - prevDep if delta < 0: delta += 4294967295 # counter wrapped if prevArr == 0: return # Ignore departure if the first arrival has not been seen hz = cfg.p_period / delta skew = delta - cfg.p_period message = "pendulum departure at " + str(t) prevDep = t if cfg.ui_showdepart: uiq.put((message, 'INFO')) # FIXME Storing 0 for clock error on departures for now, should probably do something else if cfg.db_engine: dbq.put((0, delta, hz, skew, 0)) if cfg.mqtt_engine and cfg.mqtt_p_depart: mqq.put(('beatDepart', { 'delta': delta, 'Hz': hz, 'skew': int(skew) })) # publish beat to MQTT
def dbstorebeat(beattype, delta, hz, skew, err): '''database storage routine: beattype = 1/arrive, 0/depart delta = delta since last beat, in uS hz = calculated Hz skew = skew from desired beat frequency ''' global dbx, temperature, humidity sql = "INSERT INTO beats(beattype, delta, hz, skew, temperature, humidity, error) VALUES ({}, {}, {}, {}, {}, {}, {})".format( beattype, delta, hz, skew, globs.temperature, globs.humidity, err) try: dbx except NameError: dbopen else: cur = dbx.cursor() try: debug('Executing SQL command {}'.format(sql), 3) # if cfg.ui_debugmod['db']: uiq.put(('Executing SQL command {}'.format(sql), 'DEBUG', 3)) cur.execute(sql) dbx.commit() except Error as dberr: uiq.put(('Database write error: {}'.format(dberr), 'ERR'))
def do_debug(self, *args): '''set/display the debug output level\nUsage: debug [0|1|2|3]''' if len(args) == 0: uiq.put(('Debug level is {}'.format(cfg.ui_debug))) else: try: newdbg = int(args[0]) if newdbg < 0 or newdbg > 3: raise ValueError except ValueError: uiq.put(('ERROR: Debug level must be 0-3', 'ERR')) return cfg.ui_debug = newdbg uiq.put(('Debug level set to {}'.format(cfg.ui_debug)))
def on_line_entered(self,line): if self._cmd: try: res = self._cmd(line) except Exception as e: uiq.put(('Error: %s'%e, 'ERR')) return if res==Commander.Exit: raise urwid.ExitMainLoop() elif res: uiq.put((str(res))) else: if line in ('q','quit','exit'): raise urwid.ExitMainLoop() else: uiq.put(('Unknown command \'{}\''.format(line),'NORMAL'))
def do_mqtt(self, *args): '''set/display MQTT broker parameters Usage: mqtt [cmd] [args] where cmd/args are: (nothing) display MQTT module status debug display/set MQTT debug output status (none) display MQTT debug output status [0-3] set MQTT debug output level telemetry display/set MQTT telemetry settings (none) display MQTT telemetry settings [on|off] turn MQTT telemetry on/off arr[ive] display telemetry setting for pendulum arrival [on|off] set pendulum arrival telemetry on/off dep[art] display telemetry setting for pendulum departure [on|off] set pendulum departure telemetry on/off int[erval] display MQTT telemetry interval [n] set MQTT telemetry interval ''' if len(args) == 0: if cfg.mqtt_engine: uiq.put(('MQTT engine is on')) else: uiq.pub(('MQTT engine is off')) else: cmd = args[0].lower() if cmd == 'debug': if len(args) == 1: switchdebug('MQTT', 'mqtt') else: switchdebug('MQTT', 'mqtt', args[1]) elif cmd == 'telemetry': if len(args) == 1: if cfg.mqtt_telemetry: uiq.put(( 'Telemetry is on: interval {}, arrivals {}, departures {}' .format(cfg.mqtt_telemetry_interval, mqtt_p_arrive, mqtt_p_depart))) else: uiq.put( ('Telemetry is off' )) # FIXME finish writing this stuff later :)
def do_resetdrift(self, *args): '''reset the drift statistics''' globs.driftavg = [] uiq.put(('Drift statistics reset'))
def do_pendulum(self, *args): '''set/display pendulum parameters where cmd/args are: (nothing) display pendulum sensor status arr[ive] display pendulum arrival notification status [on|off] set pendulum arrival notificions on or off dep[art] display pendulum departure notification status [on|off] set pendulum departure notifications on or off debug display pendulum sensor debug output status [0-3] set pendulum sensor debug output level ''' if len(args) == 0: pass # CHANGE what's a good default output? else: cmd = args[0].lower() if cmd == 'debug': if len(args) == 1: switchdebug('Pendulum sensor', 'p') else: switchdebug('Pendulum sensor', 'p', args[1]) elif cmd == 'arr' or cmd == 'arrive': if len(args) == 1: if cfg.ui_showarrive: uiq.put(('Pendulum arrival notifications on')) else: uiq.put(('Pendulum arrival notifications off')) return elif str(args[1]).lower() == 'on': cfg.ui_showarrive = True elif str(args[1]).lower() == 'off': cfg.ui_showarrive = False else: uiq.put(( 'ERROR: Pendulum arrival notifications must be \'off\' or \'on\'' ), 'ERR') self.do_pendulum('arr') elif cmd == 'dep' or cmd == 'depart': if len(args) == 1: if cfg.ui_showdepart: uiq.put(('Pendulum departure notifications on')) else: uiq.put(('Pendulum departure notifications off')) return elif str(args[1]).lower() == 'on': cfg.ui_showdepart = True elif str(args[1]).lower() == 'off': cfg.ui_showdepart = False else: uiq.put(( 'ERROR: Pendulum departure notifications must be \'off\' or \'on\'' ), 'ERR') self.do_pendulum('dep') else: uiq.put(('ERROR: Invalid pendulum command'))
def pendulumArrive(g, L, t): '''pendulumArrive(gpio, level, tick) - pendulum has arrived at IR sensor gate ''' global prevArr, watchdog, timeoutrpt, clocktime globs.realtime = datetime.now() loglevel = 'INFO' # Default to INFO, change to WARN or ERR if necessary timeoutrpt = 0 message = "beat detect" if prevArr: delta = t - prevArr if delta < 0: delta += 4294967295 # counter wrapped delta *= (1e6 + globs.ntpdrift) / 1e6 # Adjust for oscillator drift skew = delta - cfg.p_period if abs(skew) > cfg.p_maxskew: # Absurd arrival time, ignore uiq.put( ('Absurd arrival delta {} uS ignored.'.format(delta), 'DEBUG')) prevArr = t # need this to compute next beat watchdog.reset() # reset the watchdog timer return if abs( cfg.p_offset - skew ) > cfg.p_tolerance2: # Pendulum period is outside "bad" tolerance loglevel = 'ERR' elif abs( cfg.p_offset - skew ) > cfg.p_tolerance1: # Pendulum period is outside "warn" tolerance loglevel = 'WARN' hz = 1e6 / delta # Compute pendulum frequency (Hz) drift = (-864e2 / cfg.p_period) * skew # Compute drift (s/day) if isinstance(globs.newclocktime, datetime): # clocktime was just run globs.clocktime += globs.realtime - globs.newclocktime # add only a partial beat if cfg.ui_btcut > 0: uiq.put(('Clock time set to {}'.format( globs.clocktime.strftime(cfg.ui_btfmt)[:-cfg.ui_btcut]))) else: uiq.put(('Clock time set to {}'.format( globs.clocktime.strftime(cfg.ui_btfmt)))) globs.newclocktime = None else: globs.clocktime += timedelta( microseconds=cfg.p_period ) # add one pendlum period to the clock time # Build drift averages only when manual time *hasn't* been set (otherwise we get a ridiculous average) avgdrift(drift) beatstats = "{:+} uS / {:.6f} Hz / {:.1f} BPH / {:+.1f} s/day".format( int(skew), hz, 7200 * hz, drift) message += " ({})".format(beatstats) globs.beatbanner = "[ {} ]".format(beatstats) clockerr = (globs.clocktime - globs.realtime).total_seconds() if cfg.db_engine: dbq.put((1, delta, hz, skew, clockerr)) # store the database entry if cfg.mqtt_engine: if cfg.mqtt_p_arrive: mqq.put(( 'beatArrive', { # Publish beat to MQTT 'delta': delta, 'Hz': hz, 'skew': int(skew), 'drift': drift })) if cfg.mqtt_telemetry: globs.telemetry.append(skew) if cfg.ui_btcut > 0: mqrt = globs.realtime.strftime(cfg.ui_btfmt)[:-cfg.ui_btcut] mqct = globs.clocktime.strftime(cfg.ui_btfmt)[:-cfg.ui_btcut] mqdt = '{0:.6f}'.format(clockerr)[:-cfg.ui_btcut] else: mqrt = globs.realtime.strftime(cfg.ui_btfmt) mqct = globs.clocktime.strftime(cfg.ui_btfmt) mqdt = '{0:.6f}'.format(clockerr) mqq.put(('clocktime', { 'realtime': mqrt, 'clocktime': mqct, 'delta': mqdt })) else: watchdog = Watchdog(cfg.p_timeout, pendulumTimeout) # Start the watchdog globs.beatbanner = "[ Waiting for second beat ]" globs.clocktime = datetime.now() + timedelta(seconds=globs.driftstate) globs.driftstate = 0 # To allow for pendulum restarts to be automatically "now" prevArr = t if cfg.ui_showarrive: uiq.put((message, loglevel)) watchdog.reset() # reset the watchdog timer
def do_set(self, *args): '''set/reset/display a configuration variable\nUsage: set <variable> [arguments]''' if len(args) == 1: uiq.put(('{} is set to \'{}\''.format(args[0], cfg.__dict__[args[0]]))) return
def do_clocktime(self, *args): '''set/display the (physical) clock time Usage: clocktime Display the current physical clock time ([HH:]MM|now) Set the clock time on the next beat (+|-)s Set the clock time to the current time plus or minus s seconds ''' if len(args) == 0: if cfg.ui_btcut > 0: uiq.put(('Clock time is {}'.format( globs.clocktime.strftime(cfg.ui_btfmt)[:-cfg.ui_btcut]))) else: uiq.put(('Clock time is {}'.format( globs.clocktime.strftime(cfg.ui_btfmt)))) return elif args[0].lower() == 'now': globs.clocktime = datetime.now() elif args[0][0] == '+' or args[0][0] == '-': try: globs.clocktime = datetime.now() + timedelta( seconds=float(args[0])) except ValueError: uiq.put( ('ERROR: +/- time value must be some number of seconds', 'ERR')) return else: try: newclocktime = re.compile(r'((\d{1,2})?\D?(\d{2}))').search( args[0]).groups() except AttributeError: uiq.put(('ERROR: Time must be in format [HH:]MM', 'ERR')) return if newclocktime[1] is None: newclockhour = globs.clocktime.hour else: try: newclockhour = int(newclocktime[1]) if newclockhour < 0 or newclockhour > 23: raise ValueError except ValueError: uiq.put(('ERROR: Hour must be 0-23', 'ERR')) return try: newclockmin = int(newclocktime[2]) if newclockmin < 0 or newclockmin > 59: raise ValueError except ValueError: uiq.put(('ERROR: Minute must be 0-59', 'ERR')) return globs.clocktime = globs.clocktime.replace(hour=newclockhour, minute=newclockmin, second=0, microsecond=0) # Need to know when this ran, to compute a partial beat next time the pendulum arrives globs.newclocktime = datetime.now()
def error(msg): '''error message - display and halt''' uiq.put((msg, 'ERR'))