def __init__(self, endpoint_url=None, sensor_name=None, *args, **kwargs): self.logger = get_logger() self.logger.info(f'Setting up remote sensor {sensor_name}') # Setup the DB either from kwargs or config. self.db = None db_type = get_config('db.type', default='file') if 'db_type' in kwargs: self.logger.info(f"Setting up {kwargs['db_type']} type database") db_type = kwargs.get('db_type', db_type) self.db = PanDB(db_type=db_type) self.sensor_name = sensor_name self.sensor = None if endpoint_url is None: # Get the config for the sensor endpoint_url = get_config(f'environment.{sensor_name}.url') if endpoint_url is None: raise error.PanError(f'No endpoint_url for {sensor_name}') if not endpoint_url.startswith('http'): endpoint_url = f'http://{endpoint_url}' self.endpoint_url = endpoint_url
def test_config_reset(config_host, config_port): # Reset config via url url = f'http://{config_host}:{config_port}/reset-config' def reset_conf(): response = requests.post(url, data=serializers.to_json({'reset': True}), headers={'Content-Type': 'application/json'}) assert response.ok reset_conf() # Check we are at default value. assert get_config('location.horizon') == 30 * u.degree # Set to new value. set_config_return = set_config('location.horizon', 3 * u.degree) assert set_config_return == {'location.horizon': 3 * u.degree} # Check we have changed. assert get_config('location.horizon') == 3 * u.degree reset_conf() # Check we are at default again. assert get_config('location.horizon') == 30 * u.degree
def horizon_line(): obstruction_list = get_config('location.obstructions', default=list()) default_horizon = get_config('location.horizon') horizon_line = horizon_utils.Horizon(obstructions=obstruction_list, default_horizon=default_horizon) return horizon_line
def create_scheduler_from_config(observer=None, *args, **kwargs): """ Sets up the scheduler that will be used by the observatory """ logger = get_logger() scheduler_config = get_config('scheduler', default=None) logger.info(f'scheduler_config: {scheduler_config!r}') if scheduler_config is None or len(scheduler_config) == 0: logger.info("No scheduler in config") return None if not observer: logger.debug(f'No Observer provided, creating from config.') site_details = create_location_from_config() observer = site_details['observer'] scheduler_type = scheduler_config.get('type', 'dispatch') # Read the targets from the file fields_file = scheduler_config.get('fields_file', 'simple.yaml') fields_path = os.path.join(get_config('directories.targets'), fields_file) logger.debug(f'Creating scheduler: {fields_path}') if os.path.exists(fields_path): try: # Load the required module module = load_module(f'panoptes.pocs.scheduler.{scheduler_type}') obstruction_list = get_config('location.obstructions', default=[]) default_horizon = get_config('location.horizon', default=30 * u.degree) horizon_line = horizon_utils.Horizon( obstructions=obstruction_list, default_horizon=default_horizon.value) # Simple constraint for now constraints = [ Altitude(horizon=horizon_line), MoonAvoidance(), Duration(default_horizon, weight=5.) ] # Create the Scheduler instance scheduler = module.Scheduler(observer, fields_file=fields_path, constraints=constraints, *args, **kwargs) logger.debug("Scheduler created") except error.NotFound as e: raise error.NotFound(msg=e) else: raise error.NotFound( msg=f"Fields file does not exist: fields_file={fields_file!r}") return scheduler
def field_file(): scheduler_config = get_config('scheduler', default={}) # Read the targets from the file fields_file = scheduler_config.get('fields_file', 'simple.yaml') fields_path = os.path.join(get_config('directories.targets'), fields_file) return fields_path
def do_load_camera_env_board(self, *arg): """ Load the arduino control_board sensors """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading camera box environment board sensor") endpoint_url = get_config('environment.camera_env_board.url') self.camera_env_board = RemoteMonitor(endpoint_url=endpoint_url, sensor_name='camera_env_board', db_type=get_config( 'db.type', default='file')) self.do_enable_sensor('camera_env_board', delay=10)
def do_load_weather(self, *arg): """ Load the weather reader """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading weather reader endpoint") endpoint_url = get_config('environment.weather.url') self.weather = RemoteMonitor(endpoint_url=endpoint_url, sensor_name='weather', db_type=get_config('db.type', default='file')) self.do_enable_sensor('weather', delay=60)
def test_config_client(dynamic_config_server, config_port): assert isinstance(get_config(port=config_port), dict) assert set_config('location.horizon', 47 * u.degree, port=config_port) == { 'location.horizon': 47 * u.degree } # With parsing assert get_config('location.horizon', port=config_port) == 47 * u.degree # Without parsing assert get_config('location.horizon', port=config_port, parse=False) == '47.0 deg'
def config_getter(context, key, parse=True, default=None): """Get an item from the config server by key name, using dotted notation (e.g. 'location.elevation') If no key is given, returns the entire config. """ host = context.obj.get('host') port = context.obj.get('port') try: # The nargs=-1 makes this a tuple so we get first entry. key = key[0] except IndexError: key = None logger.debug(f'Getting config key={key!r}') try: config_entry = get_config(key=key, host=host, port=port, parse=parse, default=default) except Exception as e: logger.error(f'Error while trying to get config: {e!r}') click.secho(f'Error while trying to get config: {e!r}', fg='red') else: logger.debug(f'Config server response: config_entry={config_entry!r}') click.echo(config_entry)
def do_setup_pocs(self, *arg): """Setup and initialize a POCS instance.""" args, kwargs = string_to_params(*arg) simulator = kwargs.get('simulator', list()) if isinstance(simulator, str): simulator = [simulator] # Set whatever simulators were passed during setup client.set_config('simulator', simulator) # Retrieve what was set simulators = client.get_config('simulator', default=list()) if len(simulators): print_warning(f'Using simulators: {simulators}') if 'POCSTIME' in os.environ: print_warning("Clearing POCSTIME variable") del os.environ['POCSTIME'] try: mount = create_mount_from_config() cameras = create_cameras_from_config() scheduler = create_scheduler_from_config() observatory = Observatory(mount=mount, cameras=cameras, scheduler=scheduler) self.pocs = POCS(observatory) self.pocs.initialize() except error.PanError as e: print_warning('Problem setting up POCS: {}'.format(e))
def create_mount_simulator(mount_info=None, earth_location=None, *args, **kwargs): # Remove mount simulator current_simulators = get_config('simulator', default=[]) logger.warning(f'Current simulators: {current_simulators}') with suppress(ValueError): current_simulators.remove('mount') mount_config = mount_info or { 'model': 'Mount Simulator', 'driver': 'simulator', 'serial': { 'port': '/dev/FAKE' } } # Set mount device info to simulator set_config('mount', mount_config) earth_location = earth_location or create_location_from_config()['earth_location'] logger.debug(f"Loading mount driver: pocs.mount.{mount_config['driver']}") try: module = load_module(f"panoptes.pocs.mount.{mount_config['driver']}") except error.NotFound as e: raise error.MountNotFound(f'Error loading mount module: {e!r}') mount = module.Mount(earth_location, *args, **kwargs) logger.success(f"{mount_config['driver'].title()} mount created") return mount
def test_config_client(): assert isinstance(get_config(), dict) assert get_config('location.horizon') == 30 * u.degree assert set_config('location.horizon', 47 * u.degree) == { 'location.horizon': 47 * u.degree } assert get_config('location.horizon') == 47 * u.degree # Without parsing the result contains the double-quotes since that's what the raw # response has. assert get_config('location.horizon', parse=False) == '"47.0 deg"' assert set_config('location.horizon', 42 * u.degree, parse=False) == { 'location.horizon': '42.0 deg' }
def pocs(target): try: del os.environ['POCSTIME'] except KeyError: pass config = get_config() pocs = POCS(simulator=['weather', 'night', 'camera'], run_once=True, config=config, db='panoptes_testing') pocs.observatory.scheduler.fields_list = [ { 'name': 'Testing Target', 'position': target.to_string(style='hmsdms'), 'priority': '100', 'exptime': 2, 'min_nexp': 2, 'exp_set_size': 2, }, ] yield pocs pocs.power_down()
def observer(): loc = get_config('location') location = EarthLocation(lon=loc['longitude'], lat=loc['latitude'], height=loc['elevation']) return Observer(location=location, name="Test Observer", timezone=loc['timezone'])
def get_simulator_names(simulator=None, kwargs=None): """Returns the names of the simulators to be used in lieu of hardware drivers. Note that returning a list containing 'X' doesn't mean that the config calls for a driver of type 'X'; that is up to the code working with the config to create drivers for real or simulated hardware. This function is intended to be called from `PanBase` or similar, which receives kwargs that may include simulator, config or both. For example:: get_simulator_names(config=self.config, kwargs=kwargs) # Or: get_simulator_names(simulator=simulator, config=self.config) The reason this function doesn't just take **kwargs as its sole arg is that we need to allow for the case where the caller is passing in simulator (or config) twice, once on its own, and once in the kwargs (which won't be examined). Python doesn't permit a keyword argument to be passed in twice. >>> from panoptes.pocs.hardware import get_simulator_names >>> get_simulator_names() [] >>> get_simulator_names('all') ['camera', 'dome', 'mount', 'night', 'power', 'sensors', 'theskyx', 'weather'] Args: simulator (list): An explicit list of names of hardware to be simulated (i.e. hardware drivers to be replaced with simulators). kwargs: The kwargs passed in to the caller, which is inspected for an arg called 'simulator'. Returns: List of names of the hardware to be simulated. """ empty = dict() def extract_simulator(d): return (d or empty).get('simulator') for v in [ simulator, extract_simulator(kwargs), extract_simulator(get_config()) ]: if not v: continue if isinstance(v, str): v = [v] if 'all' in v: return ALL_NAMES else: return sorted(v) return []
def test_config_client_bad(dynamic_config_server, config_port, caplog): # Bad host will return `None` but also throw error assert set_config('foo', 42, host='foobaz') is None assert caplog.records[-1].levelname == "INFO" assert caplog.records[-1].message.startswith("Problem with set_config") # Bad host will return `None` but also throw error assert get_config('foo', host='foobaz') is None assert caplog.records[-1].levelname == "INFO" assert caplog.records[-1].message.startswith("Problem with get_config")
def do_load_camera_board(self, *arg): """ Load the arduino camera_board sensors """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading camera board sensor") self.camera_board = ArduinoSerialMonitor( sensor_name='camera_board', db_type=get_config('db.type', default='file')) self.do_enable_sensor('camera_board', delay=10)
def get_devices(self): logger.debug(f'Getting camera device connection config for {self}') camera_devices = dict() for cam_info in get_config('cameras.devices'): name = cam_info.get('name') or cam_info.get('model') port = cam_info.get('port') or cam_info.get('serial_number') camera_devices[name] = port logger.trace(f'camera_devices={camera_devices!r}') return camera_devices
def test_create_mount_with_mount_info(config_host, config_port): # Pass the mount info directly with nothing in config. mount_info = get_config('mount', default=dict()) mount_info['driver'] = 'simulator' # Remove info from config. set_config('mount', None) set_config('simulator', hardware.get_all_names(without=['mount'])) assert isinstance(create_mount_from_config(mount_info=mount_info), AbstractMount) is True reset_conf(config_host, config_port)
def test_bad_mount_driver(config_host, config_port): # Remove the mount from the list of simulators so it thinks we have a real one. simulators = get_config('simulator') with suppress(KeyError, AttributeError): simulators.pop('mount') set_config('simulator', simulators) # Set a bad port, which should cause a fail before actual mount creation. set_config('mount.serial.driver', 'foobar') with pytest.raises(error.MountNotFound): create_mount_from_config() reset_conf(config_host, config_port)
def test_update_location_no_init(mount): loc = get_config('location') location2 = EarthLocation( lon=loc['longitude'], lat=loc['latitude'], height=loc['elevation'] - 1000 * u.meter) with pytest.raises(AssertionError): mount.location = location2
def test_config_reset(dynamic_config_server, config_port, config_host): # Check we are at default. assert get_config('location.horizon', port=config_port) == 30 * u.degree # Set to new value. set_config_return = set_config('location.horizon', 47 * u.degree, port=config_port) assert set_config_return == {'location.horizon': 47 * u.degree} # Check we have changed. assert get_config('location.horizon', port=config_port) == 47 * u.degree # Reset config url = f'http://{config_host}:{config_port}/reset-config' response = requests.post(url, data=serializers.to_json({'reset': True}), headers={'Content-Type': 'application/json'}) assert response.ok # Check we are at default again. assert get_config('location.horizon', port=config_port) == 30 * u.degree
def create_dome_simulator(*args, **kwargs): dome_config = get_config('dome') brand = dome_config['brand'] driver = dome_config['driver'] logger.debug( f'Creating dome simulator: brand={brand!r}, driver={driver!r}') module = load_module(f'panoptes.pocs.dome.{driver}') dome = module.Dome(*args, **kwargs) logger.info(f'Created dome driver: brand={brand!r}, driver={driver!r}') return dome
def test_config_client_bad(caplog): # Bad host will return `None` but also throw error assert set_config('foo', 42, host='foobaz') is None assert caplog.records[-1].levelname == "WARNING" assert caplog.records[-1].message.startswith("Problem with set_config") # Bad host will return `None` but also throw error assert get_config('foo', host='foobaz') is None found_log = False for rec in caplog.records[-5:]: if rec.levelname == 'WARNING' and rec.message.startswith( 'Problem with get_config'): found_log = True assert found_log
def test_update_location(mount): loc = get_config('location') mount.initialize() location1 = mount.location location2 = EarthLocation( lon=loc['longitude'], lat=loc['latitude'], height=loc['elevation'] - 1000 * u.meter) mount.location = location2 assert location1 != location2 assert mount.location == location2
def get_config(self, *args, **kwargs): """Thin-wrapper around client based get_config that sets default port. See `panoptes.utils.config.client.get_config` for more information. Args: *args: Passed to get_config **kwargs: Passed to get_config """ config_value = None try: config_value = client.get_config(host=self._config_host, port=self._config_port, verbose=False, *args, **kwargs) except ConnectionError as e: # pragma: no cover self.logger.warning(f'Cannot connect to config_server from {self.__class__}: {e!r}') return config_value
def main(directory, upload=False, remove_jpgs=False, overwrite=False, make_timelapse=False, **kwargs): """Upload images from the given directory. See argparse help string below for details about parameters. """ pan_id = get_config('pan_id', default=None) if pan_id is None: raise error.GoogleCloudError( "Can't upload without a valid pan_id in the config") logger.debug("Cleaning observation directory: {}".format(directory)) try: clean_observation_dir(directory, remove_jpgs=remove_jpgs, include_timelapse=make_timelapse, timelapse_overwrite=overwrite, **kwargs) except Exception as e: raise error.PanError('Cannot clean observation dir: {}'.format(e)) if upload: logger.debug("Uploading to storage bucket") try: upload_observation_to_bucket(pan_id, directory, include_files='*', exclude_files='upload_manifest.log', **kwargs) except Exception as e: logger.error(f'Error in uploading observations: {e!r}') return directory
def create_dome_from_config(*args, **kwargs): """If there is a dome specified in the config, create a driver for it. A dome needs a config. We assume that there is at most one dome in the config, i.e. we don't support two different dome devices, such as might be the case if there are multiple independent actuators, for example slit, rotation and vents. Those would need to be handled by a single dome driver class. """ dome_config = get_config('dome') if dome_config is None: logger.info('No dome in config.') return None brand = dome_config['brand'] driver = dome_config['driver'] logger.debug(f'Creating dome: brand={brand!r}, driver={driver!r}') module = load_module(f'panoptes.pocs.dome.{driver}') dome = module.Dome(*args, **kwargs) logger.info(f'Created dome driver: brand={brand}, driver={driver}') return dome
def location(): loc = get_config('location') return EarthLocation(lon=loc['longitude'], lat=loc['latitude'], height=loc['elevation'])
class PanSensorShell(cmd.Cmd): """ A simple command loop for the sensors. """ intro = 'Welcome to PEAS Shell! Type ? for help' prompt = 'PEAS > ' weather = None control_board = None control_env_board = None camera_board = None camera_env_board = None active_sensors = dict() db = PanDB(db_type=get_config('db.type', default='file')) _keep_looping = False _loop_delay = 60 _timer = None captured_data = list() telemetry_relay_lookup = { 'computer': { 'pin': 8, 'board': 'telemetry_board' }, 'fan': { 'pin': 6, 'board': 'telemetry_board' }, 'camera_box': { 'pin': 7, 'board': 'telemetry_board' }, 'weather': { 'pin': 5, 'board': 'telemetry_board' }, 'mount': { 'pin': 4, 'board': 'telemetry_board' }, 'cam_0': { 'pin': 5, 'board': 'camera_board' }, 'cam_1': { 'pin': 6, 'board': 'camera_board' }, } # NOTE: These are not pins but zero-based index numbers. controlboard_relay_lookup = { 'computer': { 'pin': 0, 'board': 'control_board' }, 'mount': { 'pin': 1, 'board': 'control_board' }, 'camera_box': { 'pin': 2, 'board': 'control_board' }, 'weather': { 'pin': 3, 'board': 'control_board' }, 'fan': { 'pin': 4, 'board': 'control_board' }, } ################################################################################################## # Generic Methods ################################################################################################## def do_status(self, *arg): """ Get the entire system status and print it pretty like! """ if self._keep_looping: console.color_print("{:>12s}: ".format('Loop Timer'), "default", "active", "lightgreen") else: console.color_print("{:>12s}: ".format('Loop Timer'), "default", "inactive", "yellow") for sensor_name in ['control_board', 'camera_board', 'weather']: if sensor_name in self.active_sensors: console.color_print("{:>12s}: ".format(sensor_name.title()), "default", "active", "lightgreen") else: console.color_print("{:>12s}: ".format(sensor_name.title()), "default", "inactive", "yellow") def do_last_reading(self, device): """ Gets the last reading from the device. """ if not device: print_warning('Usage: last_reading <device>') return if not hasattr(self, device): print_warning('No such sensor: {!r}'.format(device)) return rec = self.db.get_current(device) if rec is None: print_warning('No reading found for {!r}'.format(device)) return print_info('*' * 80) print("{}:".format(device.upper())) pprint(rec) print_info('*' * 80) # Display the age in seconds of the record if isinstance(rec.get('date'), datetime.datetime): now = current_time(datetime=True).astimezone(utc) record_date = rec['date'].astimezone(utc) age = (now - record_date).total_seconds() if age < 120: print_info('{:.1f} seconds old'.format(age)) else: print_info('{:.1f} minutes old'.format(age / 60.0)) def complete_last_reading(self, text, line, begidx, endidx): """Provide completions for sensor names.""" names = list(self.active_sensors.keys()) return [name for name in names if name.startswith(text)] def do_enable_sensor(self, sensor, delay=None): """ Enable the given sensor """ if delay is None: delay = self._loop_delay if hasattr(self, sensor) and sensor not in self.active_sensors: self.active_sensors[sensor] = {'reader': sensor, 'delay': delay} def do_disable_sensor(self, sensor): """ Disable the given sensor """ if hasattr(self, sensor) and sensor in self.active_sensors: del self.active_sensors[sensor] def do_toggle_debug(self, sensor): """ Toggle DEBUG on/off for sensor Arguments: sensor {str} -- environment, weather """ # TODO(jamessynge): We currently use a single logger, not one per module or sensor. # Figure out whether to keep this code and make it work, or get rid of it. import logging get_level = { logging.DEBUG: logging.INFO, logging.INFO: logging.DEBUG, } if hasattr(self, sensor): try: log = getattr(self, sensor).logger log.setLevel(get_level[log.getEffectiveLevel()]) except Exception: print_error("Can't change log level for {}".format(sensor)) def complete_toggle_debug(self, text, line, begidx, endidx): """Provide completions for toggling debug logging.""" names = list(self.active_sensors.keys()) return [name for name in names if name.startswith(text)] ################################################################################################## # Load Methods ################################################################################################## def do_load_all(self, *arg): """Load the weather and environment sensors.""" if self._keep_looping: print_error('The timer loop is already running.') return self.do_load_weather() self.do_load_control_board() self.do_load_camera_board() def do_load_control_board(self, *arg): """ Load the arduino control_board sensors """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading control board sensor") self.control_board = ArduinoSerialMonitor(sensor_name='control_board', db_type=get_config( 'db.type', default='file')) self.do_enable_sensor('control_board', delay=10) def do_load_camera_board(self, *arg): """ Load the arduino camera_board sensors """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading camera board sensor") self.camera_board = ArduinoSerialMonitor( sensor_name='camera_board', db_type=get_config('db.type', default='file')) self.do_enable_sensor('camera_board', delay=10) def do_load_control_env_board(self, *arg): """ Load the arduino control_board sensors """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading control box environment board sensor") endpoint_url = get_config('environment.control_env_board.url') self.control_env_board = RemoteMonitor(endpoint_url=endpoint_url, sensor_name='control_env_board', db_type=get_config( 'db.type', default='file')) self.do_enable_sensor('control_env_board', delay=10) def do_load_camera_env_board(self, *arg): """ Load the arduino control_board sensors """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading camera box environment board sensor") endpoint_url = get_config('environment.camera_env_board.url') self.camera_env_board = RemoteMonitor(endpoint_url=endpoint_url, sensor_name='camera_env_board', db_type=get_config( 'db.type', default='file')) self.do_enable_sensor('camera_env_board', delay=10) def do_load_weather(self, *arg): """ Load the weather reader """ if self._keep_looping: print_error('The timer loop is already running.') return print("Loading weather reader endpoint") endpoint_url = get_config('environment.weather.url') self.weather = RemoteMonitor(endpoint_url=endpoint_url, sensor_name='weather', db_type=get_config('db.type', default='file')) self.do_enable_sensor('weather', delay=60) ################################################################################################## # Relay Methods ################################################################################################## def do_turn_off_relay(self, *arg): """Turn on relay. The argument should be the name of the relay, i.e. on of: * fan * mount * weather * camera_box The names must correspond to the entries in the lookup tables above. """ relay = arg[0] if hasattr(self, 'control_board'): relay_lookup = self.controlboard_relay_lookup else: relay_lookup = self.telemetry_relay_lookup try: relay_info = relay_lookup[relay] serial_connection = self.control_board.serial_readers[ relay_info['board']]['reader'] serial_connection.ser.reset_input_buffer() serial_connection.write("{},0\n".format(relay_info['pin'])) except Exception as e: print_warning(f"Problem turning relay off {relay} {e!r}") print_warning(e) def do_turn_on_relay(self, *arg): """Turn off relay. The argument should be the name of the relay, i.e. on of: * fan * mount * weather * camera_box The names must correspond to the entries in the lookup tables above. """ relay = arg[0] if hasattr(self, 'control_board'): relay_lookup = self.controlboard_relay_lookup else: relay_lookup = self.telemetry_relay_lookup try: relay_info = relay_lookup[relay] serial_connection = self.control_board.serial_readers[ relay_info['board']]['reader'] serial_connection.ser.reset_input_buffer() serial_connection.write("{},1\n".format(relay_info['pin'])) except Exception as e: print_warning(f"Problem turning relay off {relay} {e!r}") print_warning(e) def complete_turn_off_relay(self, text, line, begidx, endidx): """Provide completions for relay names.""" if hasattr(self, 'control_board'): names = ['camera_box', 'fan', 'mount', 'weather'] else: names = ['cam_0', 'cam_1', 'camera_box', 'fan', 'mount', 'weather'] return [name for name in names if name.startswith(text)] def complete_turn_on_relay(self, text, line, begidx, endidx): """Provide completions for relay names.""" if hasattr(self, 'control_board'): names = ['camera_box', 'fan', 'mount', 'weather'] else: names = ['cam_0', 'cam_1', 'camera_box', 'fan', 'mount', 'weather'] return [name for name in names if name.startswith(text)] def do_toggle_computer(self, *arg): """Toggle the computer relay off and then on again after 30 seconds. Note: The time delay is set on the arduino and is blocking. """ relay = 'computer' if hasattr(self, 'control_board'): relay_lookup = self.controlboard_relay_lookup else: relay_lookup = self.telemetry_relay_lookup try: relay_info = relay_lookup[relay] serial_connection = self.control_board.serial_readers[ relay_info['board']]['reader'] serial_connection.ser.reset_input_buffer() serial_connection.write("{},9\n".format(relay_info['pin'])) except Exception as e: print_warning(f"Problem toggling computer: {e!r}") print_warning(e) ################################################################################################## # Start/Stop Methods ################################################################################################## def do_start(self, *arg): """ Runs all the `active_sensors`. Blocking loop for now """ if self._keep_looping: print_error('The timer loop is already running.') return self._keep_looping = True print_info("Starting sensors") self._loop() def do_stop(self, *arg): """ Stop the loop and cancel next call """ # NOTE: We don't yet have a way to clear _timer. if not self._keep_looping and not self._timer: print_error('The timer loop is not running.') return print_info("Stopping loop") self._keep_looping = False if self._timer: self._timer.cancel() def do_change_delay(self, *arg): """Change the timing between reads from the named sensor.""" # NOTE: For at least the Arduinos, we should not need a delay and a timer, but # simply a separate thread, reading from the board as data is available. # We might use a delay to deal with the case where the device is off-line # but we want to periodically check if it becomes available. parts = None if len(arg) == 1: parts = arg[0].split() if parts is None or len(parts) != 2: print_error('Expected a sensor name and a delay, not "{}"'.format( ' '.join(arg))) return sensor_name, delay = parts try: delay = float(delay) if delay <= 0: raise ValueError() except ValueError: print_warning("Not a positive number: {!r}".format(delay)) return try: print_info("Changing sensor {} to a {} second delay".format( sensor_name, delay)) self.active_sensors[sensor_name]['delay'] = delay except KeyError: print_warning("Sensor not active: {!r}".format(sensor_name)) ################################################################################################## # Shell Methods ################################################################################################## def do_shell(self, line): """ Run a raw shell command. Can also prepend '!'. """ print("Shell command:", line) output = os.popen(line).read() print_info("Shell output: ", output) self.last_output = output def emptyline(self): self.do_status() def do_exit(self, *arg): """ Exits PEAS Shell """ print("Shutting down") if self._timer or self._keep_looping: self.do_stop() print( "Please be patient and allow for process to finish. Thanks! Bye!") return True ################################################################################################## # Private Methods ################################################################################################## def _capture_data(self, sensor_name): # We are missing a Mutex here for accessing these from active_sensors and # self. if sensor_name in self.active_sensors: sensor = getattr(self, sensor_name) try: sensor.capture(store_result=True) except Exception as e: print_warning(f'Problem storing captured data: {e!r}') self._setup_timer(sensor_name, delay=self.active_sensors[sensor_name]['delay']) def _loop(self, *arg): for sensor_name in self.active_sensors.keys(): self._capture_data(sensor_name) def _setup_timer(self, sensor_name, delay=None): if self._keep_looping and len(self.active_sensors) > 0: if not delay: delay = self._loop_delay # WARNING: It appears we have a single _timer attribute, but we create # one Timer for each active sensor (i.e. environment and weather). self._timer = Timer(delay, self._capture_data, args=(sensor_name, )) self._timer.start()