"""An IOTile gateway-in-a-box that will connect to devices using device adapters and serve them using agents."""
import logging
import threading
import pkg_resources
import tornado.ioloop
import iotilegateway.device as device
from iotile.core.exceptions import ArgumentError
def find_entry_point(group, name):
"""Find an entry point by name.
Args:
group (string): The entry point group like iotile.gateway_agent
name (string): The name of the entry point to find
"""
for entry in pkg_resources.iter_entry_points(group, name):
item = entry.load()
return item
raise ArgumentError("Could not find installed plugin by name and group", group=group, name=name)
[docs]class IOTileGateway(threading.Thread):
"""A gateway that finds IOTile devices using device adapters and serves them using agents.
The gateway runs in separate thread in a tornado IOLoop and you can call the synchronous
wait function to wait for it to quit. It will loop forever unless you stop it by calling
the stop() or stop_from_signal() methods. These functions add a task to the gateway's
event loop and implicitly call wait to synchronously wait until the gateway loop actually
stops.
IOTileGateway should be thought of as a turn-key gateway object that translates requests
for IOTile Device access received from one or more GatewayAgents into commands sent to
one or more DeviceAdapters. It is a multi-device, multi-user, multi-protocol system that
can have many connections in flight at the same time, limited only by the available resources
on the computer that hosts it.
The arguments dictionary to IOTileGateway class has the same format as the json parameters
passed to the iotile-gateway script that is just a thin wrapper around this class.
Args:
config (dict): The configuration of the gateway. There should be two keys set:
agents (list):
a list of dictionaries with the name of the agent and any arguments that
should be passed to create it.
adapters (list):
a list of dictionaries with the device adapters to add into the gateway
and any arguments that should be use to create each one.
"""
def __init__(self, config):
self.loop = None
self.device_manager = None
self.agents = []
self.loaded = threading.Event()
self._config = config
self._logger = logging.getLogger(__name__)
if 'agents' not in config:
self._config['agents'] = []
self._logger.warning("No agents defined in arguments to iotile-gateway, "
"this is likely not what you want")
elif 'adapters' not in config:
self._config['adapters'] = []
self._logger.warning("No device adapters defined in arguments to iotile-gateway, "
"this is likely not what you want")
super(IOTileGateway, self).__init__()
def run(self):
"""Start the gateway and run it to completion in another thread."""
self.loop = tornado.ioloop.IOLoop(make_current=True) # To create a loop for each thread
self.device_manager = device.DeviceManager(self.loop)
# If we have an initialization error, stop trying to initialize more things and
# just shut down cleanly
should_close = False
# Load in all of the gateway agents that are supposed to provide access to
# the devices in this gateway
for agent_info in self._config['agents']:
if 'name' not in agent_info:
self._logger.error("Invalid agent information in gateway config, info=%s, missing_key=%s",
str(agent_info), 'name')
should_close = True
break
agent_name = agent_info['name']
agent_args = agent_info.get('args', {})
self._logger.info("Loading agent by name '%s'", agent_name)
agent_class = find_entry_point('iotile.gateway_agent', agent_name)
try:
agent = agent_class(agent_args, self.device_manager, self.loop)
agent.start()
self.agents.append(agent)
except Exception: # pylint: disable=W0703
self._logger.exception("Could not load gateway agent %s, quitting", agent_name)
should_close = True
break
# Load in all of the device adapters that provide access to actual devices
if not should_close:
for adapter_info in self._config['adapters']:
if 'name' not in adapter_info:
self._logger.error("Invalid adapter information in gateway config, info=%s, missing_key=%s",
str(adapter_info), 'name')
should_close = True
break
adapter_name = adapter_info['name']
port_string = adapter_info.get('port', None)
self._logger.info("Loading device adapter by name '%s' and port '%s'", adapter_name, port_string)
try:
adapter_class = find_entry_point('iotile.device_adapter', adapter_name)
adapter = adapter_class(port_string)
self.device_manager.add_adapter(adapter)
except Exception: # pylint: disable=W0703
self._logger.exception("Could not load device adapter %s, quitting", adapter_name)
should_close = True
if should_close:
self.loop.add_callback(self._stop_loop)
else:
# Notify that we have now loaded all plugins and are starting operation (once the loop starts)
self.loop.add_callback(lambda: self.loaded.set())
self.loop.start()
# The loop has been closed, finish and quit
self._logger.critical("Done stopping loop")
def _stop_loop(self):
"""Cleanly stop the gateway and close down the IOLoop.
This function must be called only by being added to our event loop using add_callback.
"""
self._logger.critical("Stopping gateway")
self._logger.info("Stopping gateway agents")
for agent in self.agents:
try:
agent.stop()
except Exception: # pylint: disable=W0703
self._logger.exception("Error stopping gateway agent")
self._logger.critical('Stopping device adapters')
try:
self.device_manager.stop()
except Exception: # pylint: disable=W0703
self._logger.exception("Error stopping device adapters")
self.loop.stop()
self._logger.critical('Stopping event loop and shutting down')
def stop(self):
"""Stop the gateway manager and synchronously wait for it to stop."""
self.loop.add_callback(self._stop_loop)
self.wait()
def wait(self):
"""Wait for this gateway to shut down.
We need this special function because waiting inside
join will cause signals to not get handled.
"""
while self.is_alive():
try:
self.join(timeout=0.1)
except IOError:
pass # IOError comes when this call is interrupted in a signal handler
def stop_from_signal(self):
"""Stop the gateway from a signal handler, not waiting for it to stop."""
self.loop.add_callback_from_signal(self._stop_loop)