コード例 #1
0
ファイル: led_service.py プロジェクト: rubengr/gateway
class LedController(object):
    """
    The LEDController contains all logic to control the leds, and read out the physical buttons
    """
    def __init__(self, i2c_device, i2c_address, input_button):
        self._i2c_device = i2c_device
        self._i2c_address = i2c_address
        self._input_button = input_button
        self._input_button_pressed_since = None
        self._input_button_released = True
        self._ticks = 0

        self._network_enabled = False
        self._network_activity = False
        self._network_bytes = 0

        self._serial_activity = {4: False, 5: False}
        self._enabled_leds = {}
        self._previous_leds = {}
        self._last_i2c_led_code = 0

        self._indicate_started = 0
        self._indicate_pointer = 0
        self._indicate_sequence = [True, False, False, False]

        self._authorized_mode = False
        self._authorized_timeout = 0

        self._check_states_thread = None
        self._leds_thread = None
        self._button_thread = None

        self._last_run_i2c = 0
        self._last_run_gpio = 0
        self._last_state_check = 0
        self._last_button_check = 0
        self._running = False

        self._message_client = MessageClient('led_service')
        self._message_client.add_event_handler(self.event_receiver)
        self._message_client.set_state_handler(self.get_state)

        self._gpio_led_config = Hardware.get_gpio_led_config()
        self._i2c_led_config = Hardware.get_i2c_led_config()
        for led in self._gpio_led_config.keys() + self._i2c_led_config.keys():
            self._enabled_leds[led] = False
            self._write_leds()

    def start(self):
        """ Start the leds and buttons thread. """
        self._running = True
        self._check_states_thread = Thread(target=self._check_states)
        self._check_states_thread.daemon = True
        self._check_states_thread.start()

        self._leds_thread = Thread(target=self.drive_leds)
        self._leds_thread.daemon = True
        self._leds_thread.start()

        self._button_thread = Thread(target=self.check_button)
        self._button_thread.daemon = True
        self._button_thread.start()

    def stop(self):
        self._running = False

    def set_led(self, led_name, enable):
        """ Set the state of a LED, enabled means LED on in this context. """
        self._enabled_leds[led_name] = bool(enable)

    def toggle_led(self, led_name):
        """ Toggle the state of a LED. """
        self._enabled_leds[led_name] = not self._enabled_leds.get(
            led_name, False)

    def serial_activity(self, port):
        """ Report serial activity on the given serial port. Port is 4 or 5. """
        self._serial_activity[port] = True

    @staticmethod
    def _is_button_pressed(gpio_pin):
        """ Read the input button: returns True if the button is pressed, False if not. """
        with open('/sys/class/gpio/gpio{0}/value'.format(gpio_pin),
                  'r') as fh_inp:
            line = fh_inp.read()
        return int(line) == 0

    def _write_leds(self):
        """ Set the LEDs using the current status. """
        try:
            # Get i2c code
            code = 0
            for led in self._i2c_led_config:
                if self._enabled_leds.get(led, False) is True:
                    code |= self._i2c_led_config[led]
            if self._authorized_mode:
                # Light all leds in authorized mode
                for led in AUTH_MODE_LEDS:
                    code |= self._i2c_led_config.get(led, 0)
            code = (~code) & 255

            # Push code if needed
            if code != self._last_i2c_led_code:
                self._last_i2c_led_code = code
                with open(self._i2c_device, 'r+', 1) as i2c:
                    fcntl.ioctl(i2c, Hardware.IOCTL_I2C_SLAVE,
                                self._i2c_address)
                    i2c.write(chr(code))
                    self._last_run_i2c = time.time()
            else:
                self._last_run_i2c = time.time()
        except Exception as exception:
            logger.error('Error while writing to i2c: {0}'.format(exception))

        for led in self._gpio_led_config:
            on = self._enabled_leds.get(led, False)
            if self._previous_leds.get(led) != on:
                self._previous_leds[led] = on
                try:
                    gpio = self._gpio_led_config[led]
                    with open('/sys/class/gpio/gpio{0}/value'.format(gpio),
                              'w') as fh_s:
                        fh_s.write('1' if on else '0')
                        self._last_run_gpio = time.time()
                except IOError:
                    pass  # The GPIO doesn't exist or is read only
            else:
                self._last_run_gpio = time.time()

    def _check_states(self):
        """ Checks various states of the system (network) """
        while self._running:
            try:
                with open('/sys/class/net/eth0/carrier', 'r') as fh_up:
                    line = fh_up.read()
                self._network_enabled = int(line) == 1

                with open('/proc/net/dev', 'r') as fh_stat:
                    for line in fh_stat.readlines():
                        if 'eth0' in line:
                            received, transmitted = 0, 0
                            parts = line.split()
                            if len(parts) == 17:
                                received = parts[1]
                                transmitted = parts[9]
                            elif len(parts) == 16:
                                (_, received) = tuple(parts[0].split(':'))
                                transmitted = parts[8]
                            new_bytes = received + transmitted
                            if self._network_bytes != new_bytes:
                                self._network_bytes = new_bytes
                                self._network_activity = True
                            else:
                                self._network_activity = False
            except Exception as exception:
                logger.error(
                    'Error while checking states: {0}'.format(exception))
            self._last_state_check = time.time()
            time.sleep(0.5)

    def drive_leds(self):
        """ This drives different leds (status, alive and serial) """
        while self._running:
            start = time.time()
            try:
                now = time.time()
                if now - 30 < self._indicate_started < now:
                    self.set_led(
                        Hardware.Led.STATUS,
                        self._indicate_sequence[self._indicate_pointer])
                    self._indicate_pointer = self._indicate_pointer + 1 if self._indicate_pointer < len(
                        self._indicate_sequence) - 1 else 0
                else:
                    self.set_led(Hardware.Led.STATUS,
                                 not self._network_enabled)
                if self._network_activity:
                    self.toggle_led(Hardware.Led.ALIVE)
                else:
                    self.set_led(Hardware.Led.ALIVE, False)
                # Calculate serial led states
                comm_map = {4: Hardware.Led.COMM_1, 5: Hardware.Led.COMM_2}
                for uart in [4, 5]:
                    if self._serial_activity[uart]:
                        self.toggle_led(comm_map[uart])
                    else:
                        self.set_led(comm_map[uart], False)
                    self._serial_activity[uart] = False
                # Update all leds
                self._write_leds()
            except Exception as exception:
                logger.error('Error while driving leds: {0}'.format(exception))
            duration = time.time() - start
            time.sleep(max(0.05, 0.25 - duration))

    def check_button(self):
        """ Handles input button presses """
        while self._running:
            try:
                button_pressed = LedController._is_button_pressed(
                    self._input_button)
                if button_pressed is False:
                    self._input_button_released = True
                if self._authorized_mode:
                    if time.time() > self._authorized_timeout or (
                            button_pressed and self._input_button_released):
                        self._authorized_mode = False
                else:
                    if button_pressed:
                        self._ticks += 0.25
                        self._input_button_released = False
                        if self._input_button_pressed_since is None:
                            self._input_button_pressed_since = time.time()
                        if self._ticks > 5.75:  # After 5.75 seconds + time to execute the code it should be pressed between 5.8 and 6.5 seconds.
                            self._authorized_mode = True
                            self._authorized_timeout = time.time() + 60
                            self._input_button_pressed_since = None
                            self._ticks = 0
                    else:
                        self._input_button_pressed_since = None
            except Exception as exception:
                logger.error(
                    'Error while checking button: {0}'.format(exception))
            self._last_button_check = time.time()
            time.sleep(0.25)

    def event_receiver(self, event, payload):
        if event == OMBusEvents.CLOUD_REACHABLE:
            self.set_led(Hardware.Led.CLOUD, payload)
        elif event == OMBusEvents.VPN_OPEN:
            self.set_led(Hardware.Led.VPN, payload)
        elif event == OMBusEvents.SERIAL_ACTIVITY:
            self.serial_activity(payload)
        elif event == OMBusEvents.INDICATE_GATEWAY:
            self._indicate_started = time.time()

    def get_state(self):
        authorized_mode = self._authorized_mode
        if Platform.get_platform() == Platform.Type.CORE_PLUS:
            authorized_mode = True  # TODO: Should be handled by actual button

        return {
            'run_gpio': self._last_run_gpio,
            'run_i2c': self._last_run_i2c,
            'run_buttons': self._last_button_check,
            'run_state_check': self._last_state_check,
            'authorized_mode': authorized_mode
        }
コード例 #2
0
class VPNService(object):
    """ The VPNService contains all logic to be able to send the heartbeat and check whether the VPN should be opened """

    @Inject
    def __init__(self, configuration_controller=INJECTED):
        config = ConfigParser()
        config.read(constants.get_config_file())

        self._message_client = MessageClient('vpn_service')
        self._message_client.add_event_handler(self._event_receiver)
        self._message_client.set_state_handler(self._check_state)

        self._iterations = 0
        self._last_cycle = 0
        self._cloud_enabled = True
        self._sleep_time = 0
        self._previous_sleep_time = 0
        self._vpn_open = False
        self._debug_data = {}
        self._eeprom_events = deque()
        self._gateway = Gateway()
        self._vpn_controller = VpnController()
        self._config_controller = configuration_controller
        self._cloud = Cloud(config.get('OpenMotics', 'vpn_check_url') % config.get('OpenMotics', 'uuid'),
                            self._message_client,
                            self._config_controller)

        self._collectors = {'thermostats': DataCollector(self._gateway.get_thermostats, 60),
                            'inputs': DataCollector(self._gateway.get_inputs_status),
                            'outputs': DataCollector(self._gateway.get_enabled_outputs),
                            'pulses': DataCollector(self._gateway.get_pulse_counter_diff, 60),
                            'power': DataCollector(self._gateway.get_real_time_power),
                            'errors': DataCollector(self._gateway.get_errors, 600),
                            'local_ip': DataCollector(self._gateway.get_local_ip_address, 1800)}

    @staticmethod
    def ping(target, verbose=True):
        """ Check if the target can be pinged. Returns True if at least 1/4 pings was successful. """
        if target is None:
            return False

        # The popen_timeout has been added as a workaround for the hanging subprocess
        # If NTP date changes the time during a execution of a sub process this hangs forever.
        def popen_timeout(command, timeout):
            p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            for _ in xrange(timeout):
                time.sleep(1)
                if p.poll() is not None:
                    stdout_data, stderr_data = p.communicate()
                    if p.returncode == 0:
                        return True
                    raise Exception('Non-zero exit code. Stdout: {0}, stderr: {1}'.format(stdout_data, stderr_data))
            logger.warning('Got timeout during ping')
            p.kill()
            return False

        if verbose is True:
            logger.info("Testing ping to {0}".format(target))
        try:
            # Ping returns status code 0 if at least 1 ping is successful
            return popen_timeout(["ping", "-c", "3", target], 10)
        except Exception as ex:
            logger.error("Error during ping: {0}".format(ex))
            return False

    @staticmethod
    def has_connectivity():
        # Check connectivity by using ping to recover from a messed up network stack on the BeagleBone
        # Prefer using OpenMotics infrastructure first

        if VPNService.ping('cloud.openmotics.com'):
            # OpenMotics infrastructure can be pinged
            # > Connectivity
            return True
        can_ping_internet_by_fqdn = VPNService.ping('example.com') or VPNService.ping('google.com')
        if can_ping_internet_by_fqdn:
            # Public internet servers can be pinged by FQDN
            # > Assume maintenance on OpenMotics infrastructure. Sufficient connectivity
            return True
        can_ping_internet_by_ip = VPNService.ping('8.8.8.8') or VPNService.ping('1.1.1.1')
        if can_ping_internet_by_ip:
            # Public internet servers can be pinged by IP, but not by FQDN
            # > Assume DNS resolving issues. Insufficient connectivity
            return False
        # Public internet servers cannot be pinged by IP, nor by FQDN
        can_ping_default_gateway = VPNService.ping(VPNService._get_gateway())
        if can_ping_default_gateway:
            # > Assume ISP outage. Sufficient connectivity
            return True
        # > Assume broken TCP stack. No connectivity
        return False

    def _get_debug_dumps(self):
        if not self._config_controller.get_setting('cloud_support', False):
            return {}
        found_timestamps = []
        for filename in glob.glob('/tmp/debug_*.json'):
            timestamp = int(filename.replace('/tmp/debug_', '').replace('.json', ''))
            if timestamp not in self._debug_data:
                with open(filename, 'r') as debug_file:
                    self._debug_data[timestamp] = json.load(debug_file)
            found_timestamps.append(timestamp)
        for timestamp in self._debug_data:
            if timestamp not in found_timestamps:
                del self._debug_data[timestamp]
        return self._debug_data

    def _clean_debug_dumps(self):
        for timestamp in self._debug_data:
            filename = '/tmp/debug_{0}.json'.format(timestamp)
            try:
                os.remove(filename)
            except Exception as ex:
                logger.error('Could not remove debug file {0}: {1}'.format(filename, ex))

    @staticmethod
    def _get_gateway():
        """ Get the default gateway. """
        try:
            return subprocess.check_output("ip r | grep '^default via' | awk '{ print $3; }'", shell=True)
        except Exception as ex:
            logger.error("Error during get_gateway: {0}".format(ex))
            return

    def _check_state(self):
        return {'cloud_disabled': not self._cloud_enabled,
                'sleep_time': self._sleep_time,
                'cloud_last_connect': None if self._cloud is None else self._cloud.get_last_connect(),
                'vpn_open': self._vpn_open,
                'last_cycle': self._last_cycle}

    def _event_receiver(self, event, payload):
        _ = payload
        if event == OMBusEvents.DIRTY_EEPROM:
            self._eeprom_events.appendleft(True)

    @staticmethod
    def _unload_queue(queue):
        events = []
        try:
            while True:
                events.append(queue.pop())
        except IndexError:
            pass
        return events

    def _set_vpn(self, should_open):
        is_running = VpnController.check_vpn()
        if should_open and not is_running:
            logger.info("opening vpn")
            VpnController.start_vpn()
        elif not should_open and is_running:
            logger.info("closing vpn")
            VpnController.stop_vpn()
        is_running = VpnController.check_vpn()
        self._vpn_open = is_running and self._vpn_controller.vpn_connected
        self._message_client.send_event(OMBusEvents.VPN_OPEN, self._vpn_open)

    def start(self):
        self._check_vpn()

    def _check_vpn(self):
        while True:
            self._last_cycle = time.time()
            try:
                start_time = time.time()

                # Check whether connection to the Cloud is enabled/disabled
                cloud_enabled = self._config_controller.get_setting('cloud_enabled')
                if cloud_enabled is False:
                    self._sleep_time = None
                    self._set_vpn(False)
                    self._message_client.send_event(OMBusEvents.VPN_OPEN, False)
                    self._message_client.send_event(OMBusEvents.CLOUD_REACHABLE, False)

                    time.sleep(DEFAULT_SLEEP_TIME)
                    continue

                call_data = {'events': {}}

                # Events  # TODO: Replace this by websocket events in the future
                dirty_events = VPNService._unload_queue(self._eeprom_events)
                if dirty_events:
                    call_data['events']['DIRTY_EEPROM'] = True

                # Collect data to be send to the Cloud
                for collector_name in self._collectors:
                    collector = self._collectors[collector_name]
                    data = collector.collect()
                    if data is not None:
                        call_data[collector_name] = data
                call_data['debug'] = {'dumps': self._get_debug_dumps()}

                # Send data to the cloud and see if the VPN should be opened
                feedback = self._cloud.call_home(call_data)

                if feedback['success']:
                    self._clean_debug_dumps()

                if self._iterations > 20 and self._cloud.get_last_connect() < time.time() - REBOOT_TIMEOUT:
                    # We can't connect for over `REBOOT_TIMEOUT` seconds and we tried for at least 20 times.
                    # Try to figure out whether the network stack works as expected
                    if not VPNService.has_connectivity():
                        reboot_gateway()
                self._iterations += 1
                # Open or close the VPN
                self._set_vpn(feedback['open_vpn'])

                # Getting some sleep
                exec_time = time.time() - start_time
                if exec_time > 2:
                    logger.warning('Heartbeat took more than 2s to complete: {0:.2f}s'.format(exec_time))
                sleep_time = self._cloud.get_sleep_time()
                if self._previous_sleep_time != sleep_time:
                    logger.info('Set sleep interval to {0}s'.format(sleep_time))
                    self._previous_sleep_time = sleep_time
                time.sleep(sleep_time)
            except Exception as ex:
                logger.error("Error during vpn check loop: {0}".format(ex))
                time.sleep(1)